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

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

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

Объекты 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();


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