precision-soft / doctrine-utility
doctrine utilities library
Package info
github.com/precision-soft/doctrine-utility
pkg:composer/precision-soft/doctrine-utility
Requires
- php: >=8.2
- ext-pdo: *
- doctrine/dbal: 4.*
- doctrine/orm: 3.*
- doctrine/persistence: 3.*
Requires (Dev)
README
Doctrine custom types, functions, and services for MySQL.
You may fork and modify it as you wish.
Any suggestions are welcomed.
Usage for AbstractRepository and DoctrineRepository
The purposes for these classes are:
- easier constructor injection for the repositories; the quotes are because these repositories are actual read services in CRUD methodology
- code reuse by using custom filters and join filters
- better find usages for methods because you are forced to implement only what you need
Product.php
<?php declare(strict_types=1); namespace Acme\Domain\Product\Entity; use Doctrine\ORM\Mapping as ORM; use PrecisionSoft\Doctrine\Utility\Entity\CreatedTrait; use PrecisionSoft\Doctrine\Utility\Entity\ModifiedTrait; use PrecisionSoft\Doctrine\Utility\Repository\DoctrineRepository; #[ORM\Entity(repositoryClass: DoctrineRepository::class)] #[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')] #[ORM\Table(options: ['collate' => 'utf8mb4_general_ci'])] class Product { use CreatedTrait; use ModifiedTrait; #[ORM\Id] #[ORM\Column(type: 'integer', options: ['unsigned' => true])] #[ORM\GeneratedValue(strategy: 'AUTO')] private ?int $id = null; #[ORM\Column(type: 'string', length: 64, nullable: false, unique: true)] private string $barcode; #[ORM\ManyToOne(targetEntity: ProductType::class, fetch: 'EXTRA_LAZY')] #[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')] private ProductType $productType; }
ProductRepository.php
<?php declare(strict_types=1); namespace Acme\Domain\Product\Repository; use Acme\Domain\Product\Entity\Product; use Acme\Domain\Product\Exception\Exception; use Acme\Domain\Product\Exception\NotFoundException; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use PrecisionSoft\Doctrine\Utility\Join\JoinCollection; use PrecisionSoft\Doctrine\Utility\Repository\AbstractRepository; class ProductRepository extends AbstractRepository { public const JOIN_PRODUCT_TYPE = 'joinProductType'; protected function getEntityClass(): string { return Product::class; } public function find(int $productId): Product { /** @var Product|null $product */ $product = $this->getDoctrineRepository()->find($productId); if (null === $product) { throw new NotFoundException('the product was not found'); } return $product; } protected function attachCustomFilters(QueryBuilder $queryBuilder, array $filters): JoinCollection { $joins = new JoinCollection(); foreach ($filters as $key => $value) { switch ($key) { case 'barcodeLike': $baseKey = \substr($key, 0, -4); $queryBuilder ->andWhere(static::getAlias() . ".{$baseKey} LIKE :{$key}") ->setParameter($key, $value); break; case static::JOIN_PRODUCT_TYPE: $joins->addJoin( new Join( $value, static::getAlias() . '.productType', ProductTypeRepository::getAlias(), ), ); break; default: throw new Exception(\sprintf('invalid filter `%s` for `%s::%s`', $key, static::class, __FUNCTION__)); } } return $joins; } }
DQL Functions
This library provides MySQL-specific DQL functions. Register them in your Doctrine configuration:
# config/packages/doctrine.yaml doctrine: orm: dql: string_functions: JSON_CONTAINS: PrecisionSoft\Doctrine\Utility\Function\JsonContains JSON_CONTAINS_PATH: PrecisionSoft\Doctrine\Utility\Function\JsonContainsPath JSON_EXTRACT: PrecisionSoft\Doctrine\Utility\Function\JsonExtract JSON_SEARCH: PrecisionSoft\Doctrine\Utility\Function\JsonSearch JSON_UNQUOTE: PrecisionSoft\Doctrine\Utility\Function\JsonUnquote DATE_FORMAT: PrecisionSoft\Doctrine\Utility\Function\DateFormat
Available functions:
| Function | DQL Usage | Description |
|---|---|---|
JSON_CONTAINS |
JSON_CONTAINS(field, value [, path]) |
Test whether a JSON document contains a specific value |
JSON_CONTAINS_PATH |
JSON_CONTAINS_PATH(field, 'one'/'all', path [, ...]) |
Test whether a JSON document contains data at one or more paths |
JSON_EXTRACT |
JSON_EXTRACT(field, path [, ...]) |
Extract data from a JSON document |
JSON_SEARCH |
JSON_SEARCH(field, 'one'/'all', search [, escape, path...]) |
Search for a string in a JSON document |
JSON_UNQUOTE |
JSON_UNQUOTE(value) |
Unquote a JSON value |
DATE_FORMAT |
DATE_FORMAT(date, format) |
Format a date |
MysqlLockService
A service for MySQL named locks (advisory locks) via GET_LOCK() / RELEASE_LOCK().
use PrecisionSoft\Doctrine\Utility\Service\MysqlLockService; public function __construct(private MysqlLockService $lockService) {} $lockService->acquire('my-lock', timeout: 5); $hasLock = $lockService->hasLock('my-lock'); $lockService->release('my-lock'); $lockService->acquireLocks(['lock-a', 'lock-b'], timeout: 5); $lockService->releaseLocks(['lock-a', 'lock-b']); $lockService->releaseLocks();
Lock names longer than 64 characters are automatically hashed to fit MySQL's limit. Locks are reference-counted: calling acquire() multiple times with the same name increments a counter, and release() decrements it, only actually releasing the MySQL lock when the count reaches zero.
All errors throw MysqlLockException.
MySqlWalker (USE/FORCE/IGNORE INDEX)
A custom SQL walker for controlling MySQL index hints in DQL queries.
use Doctrine\ORM\Query; use PrecisionSoft\Doctrine\Utility\Walker\MySqlWalker; $query = $entityManager->createQuery('...'); $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, MySqlWalker::class); $query->setHint(MySqlWalker::HINT_USE_INDEX, 'my_index'); $query->setHint(MySqlWalker::HINT_FORCE_INDEX, 'PRIMARY'); $query->setHint(MySqlWalker::HINT_IGNORE_INDEX, 'PRIMARY, other_index'); $query->setHint(MySqlWalker::HINT_IGNORE_INDEX_ON_JOIN, ['my_index', 'joined_table']); $query->setHint(MySqlWalker::HINT_SELECT_FOR_UPDATE, true);
Index names are validated against [a-zA-Z_][a-zA-Z0-9_]* pattern for safety.
Entity Traits
CreatedTrait
Adds a created column (DATETIME, defaults to CURRENT_TIMESTAMP) with getter/setter.
use PrecisionSoft\Doctrine\Utility\Entity\CreatedTrait; class MyEntity { use CreatedTrait; }
ModifiedTrait
Adds a modified column (DATETIME, defaults to CURRENT_TIMESTAMP) with getter/setter and an automatic #[ORM\PreUpdate] callback.
Important: The consuming entity must have the #[ORM\HasLifecycleCallbacks] attribute for the automatic update to work.
use Doctrine\ORM\Mapping as ORM; use PrecisionSoft\Doctrine\Utility\Entity\ModifiedTrait; #[ORM\HasLifecycleCallbacks] class MyEntity { use ModifiedTrait; }
Dev
git clone git@github.com:precision-soft/doctrine-utility.git cd doctrine-utility ./dc build && ./dc up -d