<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
namespace Friendica;
+use Exception;
+use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Database\Database;
use Friendica\Database\DBA;
-use Friendica\Network\HTTPException;
+use Friendica\Network\HTTPException\NotFoundException;
use Psr\Log\LoggerInterface;
/**
- * Repositories are Factories linked to one or more database tables.
+ * Repositories are meant to store and retrieve Entities from the database.
+ *
+ * The reason why there are methods prefixed with an underscore is because PHP doesn't support generic polymorphism
+ * which means we can't directly overload base methods and make parameters more strict (from a parent class to a child
+ * class for example)
*
- * @see BaseModel
- * @see BaseCollection
+ * Similarly, we can't make an overloaded method return type more strict until we only support PHP version 7.4 but this
+ * is less pressing.
*/
-abstract class BaseRepository extends BaseFactory
+abstract class BaseRepository
{
const LIMIT = 30;
- /** @var Database */
- protected $dba;
-
- /** @var string */
+ /**
+ * @var string This should be set to the main database table name the depository is using
+ */
protected static $table_name;
- /** @var BaseModel */
- protected static $model_class;
+ /** @var Database */
+ protected $db;
- /** @var BaseCollection */
- protected static $collection_class;
+ /** @var LoggerInterface */
+ protected $logger;
- public function __construct(Database $dba, LoggerInterface $logger)
- {
- parent::__construct($logger);
-
- $this->dba = $dba;
- $this->logger = $logger;
- }
+ /** @var ICanCreateFromTableRow */
+ protected $factory;
- /**
- * 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)
+ public function __construct(Database $database, LoggerInterface $logger, ICanCreateFromTableRow $factory)
{
- $data = $this->dba->selectFirst(static::$table_name, [], $condition);
-
- if (!$data) {
- throw new HTTPException\NotFoundException(static::class . ' record not found.');
- }
-
- return $this->create($data);
+ $this->db = $database;
+ $this->logger = $logger;
+ $this->factory = $factory;
}
/**
- * Populates a Collection according to the condition.
+ * Populates the collection according to the condition. Retrieves a limited subset of entities depending on the
+ * boundaries and the limit. The total count of rows matching the condition is stored in the collection.
*
- * Chainable.
+ * Depends on the corresponding table featuring a numerical auto incremented column called `id`.
*
- * @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.
+ * max_id and min_id are susceptible to the query order:
+ * - min_id alone only reliably works with ASC order
+ * - max_id alone only reliably works with DESC order
+ * If the wrong order is detected in either case, we reverse the query order and the entity list order after the query
*
* Chainable.
*
- * @param array $condition
- * @param array $params
- * @param int? $max_id
- * @param int? $since_id
- * @param int $limit
+ * @param array $condition
+ * @param array $params
+ * @param int|null $min_id Retrieve models with an id no fewer than this, as close to it as possible
+ * @param int|null $max_id Retrieve models with an id no greater than this, as close to it as possible
+ * @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);
+ protected function _selectByBoundaries(
+ array $condition = [],
+ array $params = [],
+ int $min_id = null,
+ int $max_id = null,
+ int $limit = self::LIMIT
+ ): BaseCollection {
+ $totalCount = $this->count($condition);
$boundCondition = $condition;
- if (isset($max_id)) {
- $boundCondition[0] .= " AND `id` < ?";
- $boundCondition[] = $max_id;
- }
+ $reverseOrder = false;
- if (isset($since_id)) {
- $boundCondition[0] .= " AND `id` > ?";
- $boundCondition[] = $since_id;
- }
+ if (isset($min_id)) {
+ $boundCondition = DBA::mergeConditions($boundCondition, ['`id` > ?', $min_id]);
+ if (!isset($max_id) && isset($params['order']['id']) && ($params['order']['id'] === true || $params['order']['id'] === 'DESC')) {
+ $reverseOrder = true;
- $params['limit'] = $limit;
+ $params['order']['id'] = 'ASC';
+ }
+ }
- $models = $this->selectModels($boundCondition, $params);
+ if (isset($max_id) && $max_id > 0) {
+ $boundCondition = DBA::mergeConditions($boundCondition, ['`id` < ?', $max_id]);
+ if (!isset($min_id) && (!isset($params['order']['id']) || $params['order']['id'] === false || $params['order']['id'] === 'ASC')) {
+ $reverseOrder = true;
- $totalCount = DBA::count(static::$table_name, $condition);
+ $params['order']['id'] = 'DESC';
+ }
+ }
- return new static::$collection_class($models, $totalCount);
- }
+ $params['limit'] = $limit;
- /**
- * This method updates the database row from the model.
- *
- * @param BaseModel $model
- * @return bool
- * @throws \Exception
- */
- public function update(BaseModel $model)
- {
- if ($this->dba->update(static::$table_name, $model->toArray(true), ['id' => $model->id], $model->getOriginalData())) {
- $model->resetOriginalData();
- return true;
+ $Entities = $this->_select($boundCondition, $params);
+ if ($reverseOrder) {
+ $Entities->reverse();
}
- return false;
+ return new BaseCollection($Entities->getArrayCopy(), $totalCount);
}
/**
- * This method creates a new database row and returns a model if it was successful.
- *
- * @param array $fields
- * @return BaseModel|bool
- * @throws \Exception
+ * @param array $condition
+ * @param array $params
+ * @return BaseCollection
+ * @throws Exception
*/
- public function insert(array $fields)
+ protected function _select(array $condition, array $params = []): BaseCollection
{
- $return = $this->dba->insert(static::$table_name, $fields);
+ $rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
- if (!$return) {
- throw new HTTPException\InternalServerErrorException('Unable to insert new row in table "' . static::$table_name . '"');
+ $Entities = new BaseCollection();
+ foreach ($rows as $fields) {
+ $Entities[] = $this->factory->createFromTableRow($fields);
}
- $fields['id'] = $this->dba->lastInsertId();
- $return = $this->create($fields);
-
- return $return;
+ return $Entities;
}
/**
- * Deletes the model record from the database.
- *
- * @param BaseModel $model
- * @return bool
- * @throws \Exception
+ * @param array $condition
+ * @param array $params
+ * @return BaseEntity
+ * @throws NotFoundException
*/
- public function delete(BaseModel &$model)
+ protected function _selectOne(array $condition, array $params = []): BaseEntity
{
- if ($success = $this->dba->delete(static::$table_name, ['id' => $model->id])) {
- $model = null;
+ $fields = $this->db->selectFirst(static::$table_name, [], $condition, $params);
+ if (!$this->db->isResult($fields)) {
+ throw new NotFoundException();
}
- 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);
+ return $this->factory->createFromTableRow($fields);
}
/**
- * @param array $condition Query condition
- * @param array $params Additional query parameters
- * @return BaseModel[]
- * @throws \Exception
+ * @param array $condition
+ * @param array $params
+ * @return int
+ * @throws Exception
*/
- protected function selectModels(array $condition, array $params = [])
+ public function count(array $condition, array $params = []): int
{
- $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;
+ return $this->db->count(static::$table_name, $condition, $params);
}
/**
- * @param BaseCollection $collection
+ * @param array $condition
+ * @return bool
+ * @throws Exception
*/
- public function saveCollection(BaseCollection $collection)
+ public function exists(array $condition): bool
{
- $collection->map([$this, 'update']);
+ return $this->db->exists(static::$table_name, $condition);
}
}