Перейти к основному содержимому

Кастомные персистентные модели

Когда использовать кастомные модели​

Объекты Pimcore очень гибки, но их не следует использовать для хранения всех типов данных. Например, нецелесообразно реализовывать систему рейтингов, комментариев или сложную блог-систему на базе стандартных объектов Pimcore. Иногда разработчики пытаются использовать объекты только ради получения уникального ключа или построения отношений «многие-ко-многим». Это часто приводит к «грязному» коду с большим оверхедом, который медленно работает, трудно поддается рефакторингу и создает проблемы при объединении нескольких инсталляций.

Pimcore предлагает два способа работы с пользовательскими сущностями: Doctrine ORM и Pimcore DAO.

Вариант 1: Использование Doctrine ORM​

Pimcore поставляется с Doctrine Bundle, поэтому вы можете легко создавать свои сущности. Подробности можно найти в документации Symfony.

Помните, что Pimcore использует соединение Doctrine по умолчанию и стандартный менеджер сущностей (Entity Manager). Для стандартного соединения разрешено использовать только стандартный менеджер сущностей. При использовании другого менеджера с инструментом doctrine schema возникнет исключение.

Если вам нужен отдельный менеджер сущностей, используйте отдельное соединение и другую базу данных, иначе таблицы могут быть удалены.

Вариант 2: Работа с объектами доступа к данным Pimcore (DAO)​

В этом примере показано, как сохранить кастомную модель в базе данных.

База данных​

В качестве первого шага создайте структуру базы данных для модели. В этом примере я буду использовать очень простую модель под названием vote. В ней есть только идентификатор, имя пользователя (просто строка) и оценка. Если вы хотите написать модель для пакета, вам необходимо создать таблицы во время установки.

CREATE TABLE `votes` (  
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`score` int(5) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8mb4

Пожалуйста, имейте в виду, что это всего лишь общий пример, вы также можете создать другие, более сложные модели.

Модель​

Следующим шагом является реализация модели. Для упрощения работы модель хранится в src/. Вы также можете поместить её в библиотеку бандла.

# src/Model/Vote.php  
<?php

namespace App\Model;

use Pimcore\Model\AbstractModel;
use Pimcore\Model\Exception\NotFoundException;

class Vote extends AbstractModel
{
public ?int $id = null;

public ?string $username = null;

public ?int $score = null;

/**
* get score by id
*/
public static function getById(int $id): ?self
{
try {
$obj = new self;
$obj->getDao()->getById($id);
return $obj;
}
catch (NotFoundException $ex) {
\Pimcore\Logger::warn("Vote with id $id not found");
}

return null;
}

public function setScore(?int $score): void
{
$this->score = $score;
}

public function getScore(): ?int
{
return $this->score;
}

public function setUsername(?string $username): void
{
$this->username = $username;
}

public function getUsername(): ?string
{
return $this->username;
}

public function setId(?int $id): void
{
$this->id = $id;
}

public function getId(): ?int
{
return $this->id;
}
}

Для каждого поля в базе данных нам нужно соответствующее свойство и сеттер/геттер. На самом деле это не обязательно, это зависит от вашего DAO, просто продолжайте читать и посмотрите на метод сохранения в DAO.

Методы save и GetById просто вызывают соответствующие методы DAO.

Метод getDao выполняет поиск ближайшего DAO. Он просто добавляет Dao к имени класса, и если класс существует, вы готовы использовать DAO. Если класс не существует, он просто продолжит поиск, используя следующее пространство имен.

Небольшой пример: App\Model\Vote ищет App\Model\Vote\Dao, App\Model\Dao, App\Dao.

DAO​

Теперь мы готовы к реализации Dao:

#src/Model/Vote/Dao.php  
<?php

namespace App\Model\Vote;

use Pimcore\Model\Dao\AbstractDao;
use Pimcore\Model\Exception\NotFoundException;

class Dao extends AbstractDao
{
protected string $tableName = 'votes';

/**
* получение vote по id
*
* @throws NotFoundException
*/
public function getById(?int $id = null): void
{
if ($id !== null) {
$this->model->setId($id);
}

$data = $this->db->fetchAssociative('SELECT * FROM '.$this->tableName.' WHERE id = ?', [$this->model->getId()]);

if (!$data) {
throw new NotFoundException("Object with the ID " . $this->model->getId() . " doesn't exists");
}

$this->assignVariablesToModel($data);
}

/**
* сохранение vote
*/
public function save(): void
{
$vars = get_object_vars($this->model);

$buffer = [];

$validColumns = $this->getValidTableColumns($this->tableName);

if (count($vars)) {
foreach ($vars as $k => $v) {
if (!in_array($k, $validColumns)) {
continue;
}

$getter = "get" . ucfirst($k);

if (!is_callable([$this->model, $getter])) {
continue;
}

$value = $this->model->$getter();

if (is_bool($value)) {
$value = (int)$value;
}

$buffer[$k] = $value;
}
}

if ($this->model->getId() !== null) {
$this->db->update($this->tableName, $buffer, ["id" => $this->model->getId()]);
return;
}

$this->db->insert($this->tableName, $buffer);
$this->model->setId($this->db->lastInsertId());
}

/**
* удаление vote
*/
public function delete(): void
{
$this->db->delete($this->tableName, ["id" => $this->model->getId()]);
}

}

Пожалуйста, имейте в виду, что это всего лишь очень простой пример DAO. Конечно, вы можете делать гораздо более сложные вещи, такие как реализация joins, сохранение зависимостей или что угодно еще.

Использование модели​

Теперь вы можете использовать свою модель в своем слое сервисов.

$vote = new \App\Model\Vote();  
$vote->setScore(3);
$vote->setUsername('foobar!'.mt_rand(1, 999));
$vote->save();

Списки (Listing)​

Для получения списков данных необходимо реализовать классы Listing и Listing\Dao:

#src/Model/Vote/Listing.php  

<?php

namespace App\Model\Vote;

use Pimcore\Model;
use Pimcore\Model\Paginator\PaginateListingInterface;

class Listing extends Model\Listing\AbstractListing implements PaginateListingInterface
{
/**
* Список объектов Vote.
*/
public ?array $data = null;

public ?string $locale = null;

/**
* получение общего количества.
*/
public function count(): int
{
return $this->getTotalCount();
}

/**
* получение всех элементов.
*/
public function getItems(int $offset, int $itemCountPerPage): array
{
$this->setOffset($offset);
$this->setLimit($itemCountPerPage);

return $this->load();
}

/**
* Получение адаптера для разбивки на страницы.
*
* @return $this
*/
public function getPaginatorAdapter(): static
{
return $this;
}

/**
* Установка локали.
*/
public function setLocale(?string $locale): void
{
$this->locale = $locale;
}

/**
* Получение локали.
*/
public function getLocale(): ?string
{
return $this->locale;
}

/**
* Методы для итератора.
*/

/**
* Перемотка.
*/
public function rewind(): void
{
$this->getData();
reset($this->data);
}

/**
* текущий.
*/
public function current(): mixed
{
$this->getData();

return current($this->data);
}

/**
* ключ.
*/
public function key(): mixed
{
$this->getData();

return key($this->data);
}

/**
* следующий.
*/
public function next(): void
{
$this->getData();
next($this->data);
}

/**
* допустимый.
*/
public function valid(): bool
{
$this->getData();

return $this->current() !== false;
}
}

Listing\Dao​

#src/Model/Vote/Listing/Dao.php  

<?php

namespace App\Model\Vote\Listing;

use Pimcore\Model\Listing;
use App\Model;
use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
use Pimcore\Model\Listing\Dao\QueryBuilderHelperTrait;

class Dao extends Listing\Dao\AbstractDao
{
use QueryBuilderHelperTrait;

protected string $tableName = 'votes';

/**
* Получение имени таблицы как для локализованных, так и для нелокализованных данных.
*
* @throws \Exception
*/
protected function getTableName(): string
{
return $this->tableName;
}

public function getQueryBuilder(): DoctrineQueryBuilder
{
$queryBuilder = $this->db->createQueryBuilder();
$field = $this->getTableName().'.id';
$queryBuilder->select(sprintf('SQL_CALC_FOUND_ROWS %s as id', $field));
$queryBuilder->from($this->getTableName());

$this->applyListingParametersToQueryBuilder($queryBuilder);

return $queryBuilder;
}

/**
* Загрузка объектов из базы данных.
*
* @return Model\Vote[]
*/
public function load(): array
{
// загрузка id
$list = $this->loadIdList();

$objects = [];
foreach ($list as $id) {
if ($object = Model\Vote::getById($id)) {
$objects[] = $object;
}
}

$this->model->setData($objects);

return $objects;
}

/**
* Загружает список по заданным параметрам, возвращает массив идентификаторов.
*
* @return int[]
* @throws \Exception
*/
public function loadIdList(): array
{
$query = $this->getQueryBuilder();
$objectIds = $this->db->fetchFirstColumn($query->getSQL(), $query->getParameters(), $query->getParameterTypes());
$this->totalCount = (int) $this->db->fetchOne('SELECT FOUND_ROWS()');

return array_map('intval', $objectIds);
}

/**
* Получение количества.
*
* @throws \Exception
*/
public function getCount(): int
{
if ($this->model->isLoaded()) {
return count($this->model->getData());
} else {
$idList = $this->loadIdList();

return count($idList);
}
}

/**
* Получение общего количества.
*
* @throws \Exception
*/
public function getTotalCount(): int
{
$queryBuilder = $this->getQueryBuilder();
$this->prepareQueryBuilderForTotalCount($queryBuilder, $this->getTableName() . '.id');

$totalCount = $this->db->fetchOne($queryBuilder->getSql(), $queryBuilder->getParameters(), $queryBuilder->getParameterTypes());

return (int) $totalCount;
}
}

Использование списка​

Теперь вы можете использовать свой список в своем сервисном слое.

$list = \App\Model\Vote::getList();  
$list->setCondition("score > ?", [1]);
$votes = $list->load();


Вы можете предложить улучшение документации или задать вопрос в комментариях.
Если вам нужна полноценная консультация — вы можете заказать её на нашем сайте.