From c748a82e8f829a699a8e9368cb93b4842334ec22 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sun, 5 Jan 2020 17:26:51 -0500 Subject: [PATCH] Introduce Repository, Factory, Collection, Model base classes --- src/BaseCollection.php | 42 +++++++++ src/BaseFactory.php | 22 +++++ src/BaseModel.php | 53 +++++------ src/BaseRepository.php | 200 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 src/BaseCollection.php create mode 100644 src/BaseFactory.php create mode 100644 src/BaseRepository.php diff --git a/src/BaseCollection.php b/src/BaseCollection.php new file mode 100644 index 000000000..35d8dbeed --- /dev/null +++ b/src/BaseCollection.php @@ -0,0 +1,42 @@ +models = $models; + $this->totalCount = $totalCount ?? count($models); + } + + /** + * @return int + */ + public function getTotalCount() + { + return $this->totalCount; + } +} diff --git a/src/BaseFactory.php b/src/BaseFactory.php new file mode 100644 index 000000000..cfd6f9e71 --- /dev/null +++ b/src/BaseFactory.php @@ -0,0 +1,22 @@ +logger = $logger; + } +} diff --git a/src/BaseModel.php b/src/BaseModel.php index f8cef0c13..e9c3b1523 100644 --- a/src/BaseModel.php +++ b/src/BaseModel.php @@ -7,8 +7,6 @@ use Friendica\Network\HTTPException; use Psr\Log\LoggerInterface; /** - * Class BaseModel - * * The Model classes inheriting from this abstract class are meant to represent a single database record. * The associated table name has to be provided in the child class, and the table is expected to have a unique `id` field. * @@ -32,13 +30,33 @@ abstract class BaseModel */ private $data = []; - public function __construct(Database $dba, LoggerInterface $logger, $data = []) + /** + * @param Database $dba + * @param LoggerInterface $logger + * @param array $data Table row attributes + */ + public function __construct(Database $dba, LoggerInterface $logger, array $data = []) { $this->dba = $dba; $this->logger = $logger; $this->data = $data; } + /** + * Performance-improved model creation in a loop + * + * @param BaseModel $prototype + * @param array $data + * @return BaseModel + */ + public static function createFromPrototype(BaseModel $prototype, array $data) + { + $model = clone $prototype; + $model->data = $data; + + return $model; + } + /** * Magic getter. This allows to retrieve model fields with the following syntax: * - $model->field (outside of class) @@ -62,33 +80,16 @@ abstract class BaseModel } /** - * Fetches a single model record. The condition array is expected to contain a unique index (primary or otherwise). - * - * Chainable. - * - * @param array $condition - * @return BaseModel - * @throws HTTPException\NotFoundException + * @param string $name + * @param mixed $value */ - public function fetch(array $condition) + public function __set($name, $value) { - $data = $this->dba->selectFirst(static::$table_name, [], $condition); - - if (!$data) { - throw new HTTPException\NotFoundException(static::class . ' record not found.'); - } - - return new static($this->dba, $this->logger, $data); + $this->data[$name] = $value; } - /** - * Deletes the model record from the database. - * Prevents further methods from being called by wiping the internal model data. - */ - public function delete() + public function toArray() { - if ($this->dba->delete(static::$table_name, ['id' => $this->id])) { - $this->data = []; - } + return $this->data; } } diff --git a/src/BaseRepository.php b/src/BaseRepository.php new file mode 100644 index 000000000..9f43d8fe1 --- /dev/null +++ b/src/BaseRepository.php @@ -0,0 +1,200 @@ +dba = $dba; + $this->logger = $logger; + } + + /** + * Fetches a single model record. The condition array is expected to contain a unique index (primary or otherwise). + * + * Chainable. + * + * @param array $condition + * @return BaseModel + * @throws HTTPException\NotFoundException + */ + public function selectFirst(array $condition) + { + $data = $this->dba->selectFirst(static::$table_name, [], $condition); + + if (!$data) { + throw new HTTPException\NotFoundException(static::class . ' record not found.'); + } + + return $this->create($data); + } + + /** + * Populates a Collection according to the condition. + * + * Chainable. + * + * @param array $condition + * @param array $params + * @return BaseCollection + * @throws \Exception + */ + public function select(array $condition = [], array $params = []) + { + $models = $this->selectModels($condition, $params); + + return new static::$collection_class($models); + } + + /** + * Populates the collection according to the condition. Retrieves a limited subset of models depending on the boundaries + * and the limit. The total count of rows matching the condition is stored in the collection. + * + * Chainable. + * + * @param array $condition + * @param array $params + * @param int? $max_id + * @param int? $since_id + * @param int $limit + * @return BaseCollection + * @throws \Exception + */ + public function selectByBoundaries(array $condition = [], array $params = [], int $max_id = null, int $since_id = null, int $limit = self::LIMIT) + { + $condition = DBA::collapseCondition($condition); + + $boundCondition = $condition; + + if (isset($max_id)) { + $boundCondition[0] .= " AND `id` < ?"; + $boundCondition[] = $max_id; + } + + if (isset($since_id)) { + $boundCondition[0] .= " AND `id` > ?"; + $boundCondition[] = $since_id; + } + + $params['limit'] = $limit; + + $models = $this->selectModels($boundCondition, $params); + + $totalCount = DBA::count(static::$table_name, $condition); + + return new static::$collection_class($models, $totalCount); + } + + /** + * This method updates the database row from the model. + * + * @param BaseModel $model + * @return bool + * @throws \Exception + */ + public function update(BaseModel $model) + { + return $this->dba->update(static::$table_name, $model->toArray(), ['id' => $model->id], true); + } + + /** + * This method creates a new database row and returns a model if it was successful. + * + * @param array $fields + * @return BaseModel|bool + * @throws \Exception + */ + public function insert(array $fields) + { + $return = $this->dba->insert(static::$table_name, $fields); + + if ($return) { + $fields['id'] = $this->dba->lastInsertId(); + $return = $this->create($fields); + } + + return $return; + } + + /** + * Deletes the model record from the database. + * + * @param BaseModel $model + * @return bool + * @throws \Exception + */ + public function delete(BaseModel &$model) + { + if ($success = $this->dba->delete(static::$table_name, ['id' => $model->id])) { + $model = null; + } + + return $success; + } + + /** + * Base instantiation method, can be overriden to add specific dependencies + * + * @param array $data + * @return BaseModel + */ + protected function create(array $data) + { + return new static::$model_class($this->dba, $this->logger, $data); + } + + /** + * @param array $condition Query condition + * @param array $params Additional query parameters + * @return BaseModel[] + * @throws \Exception + */ + protected function selectModels(array $condition, array $params = []) + { + $result = $this->dba->select(static::$table_name, [], $condition, $params); + + /** @var BaseModel $prototype */ + $prototype = null; + + $models = []; + + while ($record = $this->dba->fetch($result)) { + if ($prototype === null) { + $prototype = $this->create($record); + $models[] = $prototype; + } else { + $models[] = static::$model_class::createFromPrototype($prototype, $record); + } + } + + return $models; + } +}