From 8fa86ca57d9df18329ae1476e3067f523408a1e0 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Tue, 11 Nov 2025 02:25:50 +0100 Subject: [PATCH 1/9] wip: Initial v2 implementation. --- .devcontainer/Dockerfile | 11 + .devcontainer/devcontainer.json | 30 + .gitignore | 4 +- composer.json | 28 +- phpunit.xml | 17 + src/LightQL/Annotations/AutoIncrement.php | 17 + .../Annotations/AutoIncrementAnnotation.php | 55 - src/LightQL/Annotations/Column.php | 40 + src/LightQL/Annotations/ColumnAnnotation.php | 92 -- src/LightQL/Annotations/EntityAnnotation.php | 94 -- src/LightQL/Annotations/Id.php | 17 + src/LightQL/Annotations/IdAnnotation.php | 54 - src/LightQL/Annotations/IdGenerator.php | 41 + .../Annotations/IdGeneratorAnnotation.php | 108 -- src/LightQL/Annotations/Index.php | 69 + src/LightQL/Annotations/ManyToMany.php | 55 + .../Annotations/ManyToManyAnnotation.php | 126 -- src/LightQL/Annotations/ManyToOne.php | 47 + .../Annotations/ManyToOneAnnotation.php | 115 -- src/LightQL/Annotations/NamedQuery.php | 43 + .../Annotations/NamedQueryAnnotation.php | 107 -- src/LightQL/Annotations/NotNull.php | 17 + src/LightQL/Annotations/NotNullAnnotation.php | 55 - src/LightQL/Annotations/OneToMany.php | 47 + .../Annotations/OneToManyAnnotation.php | 115 -- src/LightQL/Annotations/OneToOne.php | 58 + .../Annotations/OneToOneAnnotation.php | 117 -- src/LightQL/Annotations/PersistenceUnit.php | 34 + .../Annotations/PersistenceUnitAnnotation.php | 77 -- src/LightQL/Annotations/Size.php | 39 + src/LightQL/Annotations/SizeAnnotation.php | 105 -- src/LightQL/Annotations/Table.php | 38 + src/LightQL/Annotations/Unique.php | 69 + src/LightQL/Annotations/UniqueAnnotation.php | 55 - src/LightQL/Entities/Column.php | 160 +-- src/LightQL/Entities/Entity.php | 6 +- src/LightQL/Entities/EntityManager.php | 41 +- src/LightQL/Entities/IEntity.php | 55 +- src/LightQL/Entities/IEntityIdGenerator.php | 55 - src/LightQL/Entities/IPrimaryKey.php | 38 +- src/LightQL/Entities/IPrimaryKeyGenerator.php | 22 + src/LightQL/Entities/Query.php | 27 +- src/LightQL/Entities/Relation.php | 20 + src/LightQL/Enums/DBMS.php | 66 + src/LightQL/Enums/FetchMode.php | 12 + src/LightQL/Enums/JoinType.php | 17 + src/LightQL/Enums/SortOrder.php | 14 + src/LightQL/Exceptions/EntityException.php | 6 +- src/LightQL/Exceptions/FacadeException.php | 6 +- src/LightQL/Exceptions/LightQLException.php | 6 +- .../Exceptions/PersistenceUnitException.php | 6 +- src/LightQL/Exceptions/QueryException.php | 6 +- src/LightQL/LightQL.php | 1120 ++++------------- src/LightQL/Persistence/PersistenceUnit.php | 145 +-- src/LightQL/Query/Builder.php | 805 ++++++++++++ src/LightQL/Query/PendingQuery.php | 111 ++ src/LightQL/Query/QueryResult.php | 148 +++ src/LightQL/Sessions/Facade.php | 656 +++++----- src/LightQL/Sessions/IFacade.php | 102 +- 59 files changed, 2646 insertions(+), 3000 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 phpunit.xml create mode 100644 src/LightQL/Annotations/AutoIncrement.php delete mode 100644 src/LightQL/Annotations/AutoIncrementAnnotation.php create mode 100644 src/LightQL/Annotations/Column.php delete mode 100644 src/LightQL/Annotations/ColumnAnnotation.php delete mode 100644 src/LightQL/Annotations/EntityAnnotation.php create mode 100644 src/LightQL/Annotations/Id.php delete mode 100644 src/LightQL/Annotations/IdAnnotation.php create mode 100644 src/LightQL/Annotations/IdGenerator.php delete mode 100644 src/LightQL/Annotations/IdGeneratorAnnotation.php create mode 100644 src/LightQL/Annotations/Index.php create mode 100644 src/LightQL/Annotations/ManyToMany.php delete mode 100644 src/LightQL/Annotations/ManyToManyAnnotation.php create mode 100644 src/LightQL/Annotations/ManyToOne.php delete mode 100644 src/LightQL/Annotations/ManyToOneAnnotation.php create mode 100644 src/LightQL/Annotations/NamedQuery.php delete mode 100644 src/LightQL/Annotations/NamedQueryAnnotation.php create mode 100644 src/LightQL/Annotations/NotNull.php delete mode 100644 src/LightQL/Annotations/NotNullAnnotation.php create mode 100644 src/LightQL/Annotations/OneToMany.php delete mode 100644 src/LightQL/Annotations/OneToManyAnnotation.php create mode 100644 src/LightQL/Annotations/OneToOne.php delete mode 100644 src/LightQL/Annotations/OneToOneAnnotation.php create mode 100644 src/LightQL/Annotations/PersistenceUnit.php delete mode 100644 src/LightQL/Annotations/PersistenceUnitAnnotation.php create mode 100644 src/LightQL/Annotations/Size.php delete mode 100644 src/LightQL/Annotations/SizeAnnotation.php create mode 100644 src/LightQL/Annotations/Table.php create mode 100644 src/LightQL/Annotations/Unique.php delete mode 100644 src/LightQL/Annotations/UniqueAnnotation.php delete mode 100644 src/LightQL/Entities/IEntityIdGenerator.php create mode 100644 src/LightQL/Entities/IPrimaryKeyGenerator.php create mode 100644 src/LightQL/Entities/Relation.php create mode 100644 src/LightQL/Enums/DBMS.php create mode 100644 src/LightQL/Enums/FetchMode.php create mode 100644 src/LightQL/Enums/JoinType.php create mode 100644 src/LightQL/Enums/SortOrder.php create mode 100644 src/LightQL/Query/Builder.php create mode 100644 src/LightQL/Query/PendingQuery.php create mode 100644 src/LightQL/Query/QueryResult.php diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..86b1ab2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/devcontainers/php:8.4 + +# Install database dependencies +RUN apt-get update && apt-get install -y \ + libpq-dev \ + sqlite3 \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install PDO extensions for various databases +RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql pdo_sqlite mysqli opcache diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ebb2689 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "elementaryframework/lightql", + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "xdebug.php-debug", + "bmewburn.vscode-intelephense-client", + "junstyle.php-cs-fixer" + ], + "settings": { + "php.validate.executablePath": "/usr/local/bin/php" + } + }, + "jetbrains": { + "backend": "PhpStorm", + "plugins": [ + "com.jetbrains.php" + ] + } + }, + "postCreateCommand": "composer install", + "remoteUser": "vscode" +} diff --git a/.gitignore b/.gitignore index c0ef46d..24eed42 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ composer.lock # Test files and folders tests/ +examples/ +ligthql.db # Docs (hidden for now) -docs/ \ No newline at end of file +docs/ diff --git a/composer.json b/composer.json index 56e22e7..9f4c1ea 100644 --- a/composer.json +++ b/composer.json @@ -2,19 +2,21 @@ "name": "elementaryframework/light-ql", "description": "The lightweight PHP ORM", "type": "library", - "require-dev": { - "phpunit/phpunit": "^7" - }, "license": "MIT", "authors": [ { "name": "Axel Nana", - "email": "ax.lnana@outlook.com" + "email": "axel.nana@aliens-group.com" } ], "require": { - "php": "^7.1.10", - "elementaryframework/annotations": "^2.0.1" + "php": "^8.4", + "ext-pdo": "*" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-type-coverage": "^3.0" }, "keywords": [ "orm", @@ -25,5 +27,19 @@ "psr-4": { "ElementaryFramework\\LightQL\\": "src/LightQL/" } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "scripts": { + "test": "pest", + "test:coverage": "pest --coverage" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ce5bb81 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + src + + + diff --git a/src/LightQL/Annotations/AutoIncrement.php b/src/LightQL/Annotations/AutoIncrement.php new file mode 100644 index 0000000..114f37e --- /dev/null +++ b/src/LightQL/Annotations/AutoIncrement.php @@ -0,0 +1,17 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; - -/** - * Auto Increment Annotation - * - * Used to define that a property is an auto - * incremented table column. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/AutoIncrementAnnotation - */ -class AutoIncrementAnnotation extends Annotation -{ -} diff --git a/src/LightQL/Annotations/Column.php b/src/LightQL/Annotations/Column.php new file mode 100644 index 0000000..59995e3 --- /dev/null +++ b/src/LightQL/Annotations/Column.php @@ -0,0 +1,40 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Column Annotation - * - * Used to define that a property is a table column. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/ColumnAnnotation - */ -class ColumnAnnotation extends Annotation -{ - /** - * The name of the column. - * - * @var string - */ - public $name; - - /** - * The type of the column. - * - * @var string - */ - public $type; - - /** - * The default value of the column. - * - * @var mixed - */ - public $default = null; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('name', 'type', 'default')); - - parent::initAnnotation($properties); - - if (!isset($this->name)) { - throw new AnnotationException(self::class . " requires a \"name\" property"); - } - } -} diff --git a/src/LightQL/Annotations/EntityAnnotation.php b/src/LightQL/Annotations/EntityAnnotation.php deleted file mode 100644 index 3f09da0..0000000 --- a/src/LightQL/Annotations/EntityAnnotation.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; -use ElementaryFramework\LightQL\Entities\Entity; - -/** - * Entity Annotation - * - * Used to define a class as an entity. - * - * @usage('class' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/EntityAnnotation - */ -class EntityAnnotation extends Annotation -{ - /** - * The table name represented by the entity. - * - * @var string - */ - public $table; - - /** - * The fetch mode used by the entity. - * - * @var integer - */ - public $fetchMode = Entity::FETCH_LAZY; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('table', 'fetchMode')); - - parent::initAnnotation($properties); - - if ($this->fetchMode === "LAZY") { - $this->fetchMode = Entity::FETCH_LAZY; - } - - if ($this->fetchMode === "EAGER") { - $this->fetchMode = Entity::FETCH_EAGER; - } - - if (!isset($this->table)) { - throw new AnnotationException(self::class . " requires a \"table\" property"); - } - } -} diff --git a/src/LightQL/Annotations/Id.php b/src/LightQL/Annotations/Id.php new file mode 100644 index 0000000..799638a --- /dev/null +++ b/src/LightQL/Annotations/Id.php @@ -0,0 +1,17 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; - -/** - * Id Annotation - * - * Used to define a property as the primary key of a table. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/IdAnnotation - */ -class IdAnnotation extends Annotation -{ -} diff --git a/src/LightQL/Annotations/IdGenerator.php b/src/LightQL/Annotations/IdGenerator.php new file mode 100644 index 0000000..f7d6cdf --- /dev/null +++ b/src/LightQL/Annotations/IdGenerator.php @@ -0,0 +1,41 @@ + $generator Specify the class name to use as the ID generator of the current entity. + */ +#[Attribute(Attribute::TARGET_CLASS)] +class IdGenerator +{ + /** + * Initialize the id generator attribute. + * + * @param class-string $generator The ID generator class name + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $generator + ) { + if (empty($generator)) { + throw new InvalidArgumentException(self::class . " must have a non-empty \"generator\" property"); + } + + if (!is_subclass_of($generator, IPrimaryKeyGenerator::class)) { + throw new InvalidArgumentException("Generator class \"$generator\" must implement " . IPrimaryKeyGenerator::class); + } + } +} diff --git a/src/LightQL/Annotations/IdGeneratorAnnotation.php b/src/LightQL/Annotations/IdGeneratorAnnotation.php deleted file mode 100644 index b804d5f..0000000 --- a/src/LightQL/Annotations/IdGeneratorAnnotation.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\AnnotationFile; -use ElementaryFramework\Annotations\IAnnotationFileAware; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Id Generator Annotation - * - * Used to define the primary key generator of an entity. - * - * This annotation have to be associated with the @entity - * annotation to take effect. - * - * The entity class with this annotation must have a - * property with the @id annotation. - * - * @usage('class' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/IdAnnotation - */ -class IdGeneratorAnnotation extends Annotation implements IAnnotationFileAware -{ - /** - * Specify the class name to use as the ID generator - * of the current entity. - * - * @var string - */ - public $generator; - - /** - * Annotation file. - * - * @var AnnotationFile - */ - protected $file; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array("generator")); - - parent::initAnnotation($properties); - - if (!isset($this->generator)) { - throw new AnnotationException(self::class . " must have a \"generator\" property"); - } - - $this->generator = $this->file->resolveType($this->generator); - } - - /** - * Provides information about file, that contains this annotation. - * - * @param AnnotationFile $file Annotation file. - * - * @return void - */ - public function setAnnotationFile(AnnotationFile $file) - { - $this->file = $file; - } -} diff --git a/src/LightQL/Annotations/Index.php b/src/LightQL/Annotations/Index.php new file mode 100644 index 0000000..a10ce77 --- /dev/null +++ b/src/LightQL/Annotations/Index.php @@ -0,0 +1,69 @@ +columns = $columns; + $this->name = $name; + } +} diff --git a/src/LightQL/Annotations/ManyToMany.php b/src/LightQL/Annotations/ManyToMany.php new file mode 100644 index 0000000..33b12a3 --- /dev/null +++ b/src/LightQL/Annotations/ManyToMany.php @@ -0,0 +1,55 @@ + $entity The referenced entity class name. + * @property-read class-string $pivotTable The name of the pivot table, or the class name of the pivot entity. + * @property-read string $foreignColumn The name of the column in the pivot table. + * @property-read string|null $localColumn The name of the column in the entity table. Set to null to use the primary key of the entity table. + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class ManyToMany +{ + /** + * Initialize the many-to-many attribute. + * + * @param class-string $entity The referenced entity class name. + * @param class-string $pivotTable The name of the pivot table, or the class name of the pivot entity. + * @param string $foreignColumn The name of the column in the pivot table. + * @param string|null $localColumn The name of the column in the entity table. Set to null to use the primary key of the entity table. + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $entity, + public readonly string $pivotTable, + public readonly string $foreignColumn, + public readonly ?string $localColumn = null, + ) + { + if (empty($entity)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + } + + if (empty($pivotTable)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"pivotTable\" property"); + } + + if (empty($foreignColumn)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"foreignColumn\" property"); + } + } +} diff --git a/src/LightQL/Annotations/ManyToManyAnnotation.php b/src/LightQL/Annotations/ManyToManyAnnotation.php deleted file mode 100644 index 58f86ac..0000000 --- a/src/LightQL/Annotations/ManyToManyAnnotation.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\AnnotationFile; -use ElementaryFramework\Annotations\IAnnotationFileAware; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Many-To-Many Annotation - * - * Used to define that a property is in a many-to-many relation with another. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/ManyToManyAnnotation - */ -class ManyToManyAnnotation extends Annotation implements IAnnotationFileAware -{ - /** - * The referenced entity of the many-to-many relation. - * - * @var string - */ - public $entity; - - /** - * The name of the table born from the many-to-many relation. - * - * @var string - */ - public $crossTable; - - /** - * The referenced column name of the many-to-many relation. - * - * @var string - */ - public $referencedColumn; - - /** - * Annotation file. - * - * @var AnnotationFile - */ - protected $file; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('entity', 'crossTable', 'referencedColumn')); - - parent::initAnnotation($properties); - - if (!isset($this->crossTable)) { - throw new AnnotationException(self::class . " requires a \"crossTable\" property"); - } - - if (!isset($this->referencedColumn)) { - throw new AnnotationException(self::class . " requires a \"referencedColumn\" property"); - } - - if (!isset($this->entity)) { - throw new AnnotationException(self::class . " requires a \"entity\" property"); - } - - $this->entity = $this->file->resolveType($this->entity); - } - - /** - * Provides information about file, that contains this annotation. - * - * @param AnnotationFile $file Annotation file. - * - * @return void - */ - public function setAnnotationFile(AnnotationFile $file) - { - $this->file = $file; - } -} diff --git a/src/LightQL/Annotations/ManyToOne.php b/src/LightQL/Annotations/ManyToOne.php new file mode 100644 index 0000000..1fcc8d5 --- /dev/null +++ b/src/LightQL/Annotations/ManyToOne.php @@ -0,0 +1,47 @@ +|string $entity The referenced entity class name or table name. + * @property-read string $referencedColumn The name of the referenced column. + * @property-read string|null $localColumn The name of the local column. If null, the primary key of the entity is used. + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class ManyToOne +{ + /** + * Initialize the many-to-one attribute. + * + * @param class-string|string $entity The referenced entity class name + * @param string|null $localColumn The name of the local column. If null, the primary key of the entity is used. + * @param string $referencedColumn The name of the referenced column + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $entity, + public readonly string $referencedColumn, + public readonly ?string $localColumn = null, + ) { + if (empty($entity)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + } + + if (empty($referencedColumn)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" property"); + } + } +} diff --git a/src/LightQL/Annotations/ManyToOneAnnotation.php b/src/LightQL/Annotations/ManyToOneAnnotation.php deleted file mode 100644 index 822134f..0000000 --- a/src/LightQL/Annotations/ManyToOneAnnotation.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\IAnnotationFileAware; -use ElementaryFramework\Annotations\AnnotationFile; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Many-To-One Annotation - * - * Used to define that a property is in a many-to-one relation with another. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/ManyToOneAnnotation - */ -class ManyToOneAnnotation extends Annotation implements IAnnotationFileAware -{ - /** - * The referenced entity in this many-to-one relation. - * - * @var string - */ - public $entity; - - /** - * The name of the referenced column. - * - * @var string - */ - public $referencedColumn; - - /** - * Annotation file. - * - * @var AnnotationFile - */ - protected $file; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('entity', 'referencedColumn')); - - parent::initAnnotation($properties); - - if (!isset($this->referencedColumn)) { - throw new AnnotationException(self::class . " requires a \"referencedColumn\" property"); - } - - if (!isset($this->entity)) { - throw new AnnotationException(self::class . " requires a \"entity\" property"); - } - - $this->entity = $this->file->resolveType($this->entity); - } - - /** - * Provides information about file, that contains this annotation. - * - * @param AnnotationFile $file Annotation file. - * - * @return void - */ - public function setAnnotationFile(AnnotationFile $file) - { - $this->file = $file; - } -} diff --git a/src/LightQL/Annotations/NamedQuery.php b/src/LightQL/Annotations/NamedQuery.php new file mode 100644 index 0000000..111ad9d --- /dev/null +++ b/src/LightQL/Annotations/NamedQuery.php @@ -0,0 +1,43 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Named Query Annotation - * - * Used to list the set of SQL queries associated to - * an entity. - * - * This annotation have to be associated with the @entity - * annotation to take effect. - * - * @usage('class' => true, 'multiple' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/NamedQueryAnnotation - */ -class NamedQueryAnnotation extends Annotation -{ - /** - * The query's name. - * - * @var string - */ - public $name; - - /** - * The SQL query. - * - * @var string - */ - public $query; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties. - * - * @throws AnnotationException - */ - public function initAnnotation(array $properties) - { - if (isset($properties[0])) { - $this->name = strval($properties[0]); - unset($properties[0]); - } - - if (isset($properties[1])) { - $this->query = strval($properties[1]); - unset($properties[1]); - } - - parent::initAnnotation($properties); - - if ($this->name !== null && strlen($this->name) <= 0) { - throw new AnnotationException(self::class . ' requires a (string) name property.'); - } - - if ($this->name === null) { - throw new AnnotationException(self::class . ' requires a (string) name property.'); - } - - if ($this->query !== null && strlen($this->query) <= 0) { - throw new AnnotationException(self::class . ' requires a (string) query property.'); - } - - if ($this->query === null) { - throw new AnnotationException(self::class . ' requires a (string) query property.'); - } - } -} diff --git a/src/LightQL/Annotations/NotNull.php b/src/LightQL/Annotations/NotNull.php new file mode 100644 index 0000000..0079e20 --- /dev/null +++ b/src/LightQL/Annotations/NotNull.php @@ -0,0 +1,17 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Not Null Annotation - * - * Used to set a property as a not null value. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/NotNullAnnotation - */ -class NotNullAnnotation extends Annotation -{ -} diff --git a/src/LightQL/Annotations/OneToMany.php b/src/LightQL/Annotations/OneToMany.php new file mode 100644 index 0000000..4923bc5 --- /dev/null +++ b/src/LightQL/Annotations/OneToMany.php @@ -0,0 +1,47 @@ + $entity The referenced entity class name. + * @property-read string $mappedBy The name of the property in the referenced entity that maps back to this entity. + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class OneToMany +{ + /** + * Initialize the one-to-many attribute. + * + * @param class-string $entity The referenced entity class name. + * @param string $mappedBy The name of the property in the referenced entity that maps back to this entity. + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $entity, + public readonly string $mappedBy + ) + { + if (empty($entity)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + } + + if (empty($mappedBy)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"mappedBy\" property"); + } + } +} diff --git a/src/LightQL/Annotations/OneToManyAnnotation.php b/src/LightQL/Annotations/OneToManyAnnotation.php deleted file mode 100644 index 8ec9483..0000000 --- a/src/LightQL/Annotations/OneToManyAnnotation.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\AnnotationFile; -use ElementaryFramework\Annotations\IAnnotationFileAware; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * One-To-Many Annotation - * - * Used to define that a property is in a one-to-many relation with another. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/OneToManyAnnotation - */ -class OneToManyAnnotation extends Annotation implements IAnnotationFileAware -{ - /** - * The referenced entity in this one-to-many relation. - * - * @var string - */ - public $entity; - - /** - * The referenced column name of the many-to-many relation. - * - * @var string - */ - public $referencedColumn; - - /** - * Annotation file. - * - * @var AnnotationFile - */ - protected $file; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('entity', 'referencedColumn')); - - parent::initAnnotation($properties); - - if (!isset($this->entity)) { - throw new AnnotationException(self::class . " requires a \"entity\" property"); - } - - if (!isset($this->referencedColumn)) { - throw new AnnotationException(self::class . " requires a \"referencedColumn\" property"); - } - - $this->entity = $this->file->resolveType($this->entity); - } - - /** - * Provides information about file, that contains this annotation. - * - * @param AnnotationFile $file Annotation file. - * - * @return void - */ - public function setAnnotationFile(AnnotationFile $file) - { - $this->file = $file; - } -} diff --git a/src/LightQL/Annotations/OneToOne.php b/src/LightQL/Annotations/OneToOne.php new file mode 100644 index 0000000..bcdc44c --- /dev/null +++ b/src/LightQL/Annotations/OneToOne.php @@ -0,0 +1,58 @@ +|string $entity The referenced entity class name or table name. + * @property-read string|null $referencedColumn The name of the referenced column. + * @property-read string|null $mappedBy The name of the property in the referenced entity that maps this relation. + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class OneToOne +{ + /** + * Initialize the one-to-one attribute. + * + * @param class-string|string $entity The referenced entity class name or table name. + * @param string|null $referencedColumn The name of the referenced column. + * @param string|null $mappedBy The name of the property in the referenced entity that maps this relation. + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $entity, + public readonly ?string $referencedColumn = null, + public readonly ?string $mappedBy = null + ) { + if (empty($entity)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + } + + if ($referencedColumn === null && $mappedBy === null) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" or \"mappedBy\" property"); + } + + if ($referencedColumn !== null && $mappedBy !== null) { + throw new InvalidArgumentException(self::class . " requires either a \"referencedColumn\" or \"mappedBy\" property, not both"); + } + + if ($referencedColumn !== null && empty($referencedColumn)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" property"); + } + + if ($mappedBy !== null && empty($mappedBy)) { + throw new InvalidArgumentException(self::class . " requires a non-empty \"mappedBy\" property"); + } + } +} diff --git a/src/LightQL/Annotations/OneToOneAnnotation.php b/src/LightQL/Annotations/OneToOneAnnotation.php deleted file mode 100644 index 9c2c5b2..0000000 --- a/src/LightQL/Annotations/OneToOneAnnotation.php +++ /dev/null @@ -1,117 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\IAnnotationFileAware; -use ElementaryFramework\Annotations\AnnotationFile; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * One-To-One Annotation - * - * Used to define that a property is in a one-to-one relation with another. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/OneToOneAnnotation - */ -class OneToOneAnnotation extends Annotation implements IAnnotationFileAware -{ - /** - * The entity class name referenced in - * this one-to-one relation. - * - * @var string - */ - public $entity; - - /** - * The column referenced in this - * one-to-one relation. - * - * @var string - */ - public $referencedColumn; - - /** - * Annotation file. - * - * @var AnnotationFile - */ - protected $file; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties - * - * @throws AnnotationException - * - * @return void - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array('entity', 'referencedColumn')); - - parent::initAnnotation($properties); - - if (!isset($this->referencedColumn)) { - throw new AnnotationException(self::class . " requires a \"referencedColumn\" property"); - } - - if (!isset($this->entity)) { - throw new AnnotationException(self::class . " requires a \"entity\" property"); - } - - $this->entity = $this->file->resolveType($this->entity); - } - - /** - * Provides information about file, that contains this annotation. - * - * @param AnnotationFile $file Annotation file. - * - * @return void - */ - public function setAnnotationFile(AnnotationFile $file) - { - $this->file = $file; - } -} diff --git a/src/LightQL/Annotations/PersistenceUnit.php b/src/LightQL/Annotations/PersistenceUnit.php new file mode 100644 index 0000000..e2b339d --- /dev/null +++ b/src/LightQL/Annotations/PersistenceUnit.php @@ -0,0 +1,34 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Persistence Unit Annotation - * - * Used to set a property as the entity manager - * of the given persistence unit. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/PersistenceUnitAnnotation - */ -class PersistenceUnitAnnotation extends Annotation -{ - /** - * The name of this persistence unit. - * - * @var string - */ - public $name; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties. - * - * @throws AnnotationException - */ - public function initAnnotation(array $properties) - { - $this->map($properties, array("name")); - - parent::initAnnotation($properties); - - if (($this->name !== null && strlen($this->name) <= 0) || $this->name === null) { - throw new AnnotationException(self::class . ' requires a (string) name property.'); - } - } -} diff --git a/src/LightQL/Annotations/Size.php b/src/LightQL/Annotations/Size.php new file mode 100644 index 0000000..5e1afc4 --- /dev/null +++ b/src/LightQL/Annotations/Size.php @@ -0,0 +1,39 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -/** - * Size Annotation - * - * Used to set the size of a mapped property. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/SizeAnnotation - */ -class SizeAnnotation extends Annotation -{ - /** - * The minimal size of the value. - * - * @var integer - */ - public $min = null; - - /** - * The maximal size of the value. - * - * @var integer - */ - public $max = null; - - /** - * Initialize the annotation. - * - * @param array $properties The array of annotation properties. - * - * @throws AnnotationException - */ - public function initAnnotation(array $properties) - { - if (isset($properties[0])) { - if (isset($properties[1])) { - $this->min = $properties[0]; - $this->max = $properties[1]; - unset($properties[1]); - } else { - $this->min = 0; - $this->max = $properties[0]; - } - - unset($properties[0]); - } - - parent::initAnnotation($properties); - - if ($this->min !== null && !is_int($this->min)) { - throw new AnnotationException(self::class . ' requires an (integer) min property'); - } - - if ($this->max !== null && !is_int($this->max)) { - throw new AnnotationException(self::class . ' requires an (integer) max property'); - } - - if ($this->min === null && $this->max === null) { - throw new AnnotationException(self::class . ' requires a min and/or max property'); - } - } -} diff --git a/src/LightQL/Annotations/Table.php b/src/LightQL/Annotations/Table.php new file mode 100644 index 0000000..8f30dab --- /dev/null +++ b/src/LightQL/Annotations/Table.php @@ -0,0 +1,38 @@ +columns = $columns; + $this->name = $name; + } +} diff --git a/src/LightQL/Annotations/UniqueAnnotation.php b/src/LightQL/Annotations/UniqueAnnotation.php deleted file mode 100644 index f6c5ff6..0000000 --- a/src/LightQL/Annotations/UniqueAnnotation.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Annotations; - -use ElementaryFramework\Annotations\Annotation; - -/** - * Unique Annotation - * - * Used to define that a property represents an unique column - * in the table. - * - * This annotation have to be associated with the @column - * annotation to take effect. - * - * @usage('property' => true, 'inherited' => true) - * - * @category Annotations - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/UniqueAnnotation - */ -class UniqueAnnotation extends Annotation -{ -} diff --git a/src/LightQL/Entities/Column.php b/src/LightQL/Entities/Column.php index 1a971a9..a836823 100644 --- a/src/LightQL/Entities/Column.php +++ b/src/LightQL/Entities/Column.php @@ -1,186 +1,90 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Entities; /** * Column * - * Describe a database table's column. + * Describe a single table's column from the database. * - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/Column + * @template T */ class Column { /** * The column name. - * - * @var string */ - private $_name; + private(set) string $name; /** * The column type. - * - * @var string */ - private $_type; + private(set) string $type; /** * The column size (if any). * - * @var int[] + * @var array{int, int} */ - private $_size; + private(set) array $size; /** * The column's default value. * - * @var mixed + * @var T */ - private $_default = null; + private(set) mixed $default = null; /** - * Defines if the column has the - * AUTO_INCREMENT property. - * - * @var bool + * Defines if the column has the AUTO_INCREMENT constraint. */ - public $isAutoIncrement; + public bool $isAutoIncrement; /** - * Defines if the column is a - * primary key. - * - * @var bool + * Defines if the column is a primary key. */ - public $isPrimaryKey; + public bool $isPrimaryKey; /** - * Defines if the column has the - * UNIQUE property. - * - * @var bool + * Defines if the column has the UNIQUE constraint. */ - public $isUniqueKey; + public bool $isUniqueKey; /** - * Defines if the column is in - * a one-to-many relation with another. - * - * @var bool + * Defines if the column is in a one-to-many relation with another. */ - public $isOneToMany; + public bool $isOneToMany; /** - * Defines if the column is in - * a many-to-one relation with another. - * - * @var bool + * Defines if the column is in a many-to-one relation with another. */ - public $isManyToOne; + public bool $isManyToOne; /** - * Defines if the column is in - * a many-to-many relation with another. - * - * @var bool + * Defines if the column is in a many-to-many relation with another. */ - public $isManyToMany; + public bool $isManyToMany; /** - * Defines if the column is in - * a one-to-one relation with another. - * - * @var bool + * Defines if the column is in a one-to-one relation with another. */ - public $isOneToOne; + public bool $isOneToOne; /** * Create a new instance of the table column descriptor. * - * @param string $name The column's name. - * @param string $type The column's type. - * @param int[] $size The array of sizes containing (min, max) values only. - * @param mixed $default The default value of the column. - */ - public function __construct(string $name, string $type, array $size, $default = null) - { - $this->_name = $name; - $this->_type = $type; - $this->_size = $size; - $this->_default = $default; - } - - /** - * Returns the column's name. - * - * @return string - */ - public function getName(): string - { - return $this->_name; - } - - /** - * Returns the column's type. - * - * @return string - */ - public function getType(): string - { - return $this->_type; - } - - /** - * Returns the column's size. - * - * @return array - */ - public function getSize(): array - { - return $this->_size; - } - - /** - * Returns the column's default value. - * - * @return mixed + * @param string $name The column's name. + * @param string $type The column's type. + * @param array{int, int} $size The array of sizes containing (min, max) values only. + * @param null|T $default The default value of the column. */ - public function getDefault() + public function __construct(string $name, string $type, array $size, mixed $default = null) { - return $this->_default; + $this->name = $name; + $this->type = $type; + $this->size = $size; + $this->default = $default; } } diff --git a/src/LightQL/Entities/Entity.php b/src/LightQL/Entities/Entity.php index 94d6ebf..bc2ae51 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -45,7 +45,7 @@ * @abstract * @category Entities * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/Entity */ abstract class Entity implements IEntity diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 3acd650..3244b13 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -33,6 +33,7 @@ namespace ElementaryFramework\LightQL\Entities; use ElementaryFramework\Annotations\Annotations; +use ElementaryFramework\LightQL\Enums\DBMS; use ElementaryFramework\LightQL\Exceptions\EntityException; use ElementaryFramework\LightQL\LightQL; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; @@ -45,7 +46,7 @@ * @final * @category Entities * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/EntityManager */ final class EntityManager @@ -56,7 +57,7 @@ final class EntityManager * * @var PersistenceUnit */ - private $_persistenceUnit; + private PersistenceUnit $_persistenceUnit; /** * The LightQL instance used by this @@ -80,13 +81,11 @@ public function __construct(PersistenceUnit $persistenceUnit) // Create a LightQL instance $this->_lightql = new LightQL( - array( - "dbms" => $this->_persistenceUnit->getDbms(), - "database" => $this->_persistenceUnit->getDatabase(), - "hostname" => $this->_persistenceUnit->getHostname(), - "username" => $this->_persistenceUnit->getUsername(), - "password" => $this->_persistenceUnit->getPassword() - ) + database: $this->_persistenceUnit->database, + hostname: $this->_persistenceUnit->hostname, + username: $this->_persistenceUnit->username, + password: $this->_persistenceUnit->password, + dbms: $this->_persistenceUnit->dbms, ); } @@ -94,7 +93,7 @@ public function __construct(PersistenceUnit $persistenceUnit) * Finds an entity from the database. * * @param string $entityClass The class name of the entity to find. - * @param mixed $id The value of the primary key. + * @param mixed $id The value of the primary key. * * @return array Raw data from database. * @@ -144,12 +143,12 @@ public function find(string $entityClass, $id): array /** * Persists an entity into the database. * - * @param Entity $entity The entity to create. + * @param IEntity $entity The entity to create. * * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException * @throws \ElementaryFramework\LightQL\Exceptions\EntityException */ - public function persist(Entity &$entity) + public function persist(IEntity &$entity) { $entityAnnotation = Annotations::ofClass($entity, "@entity"); @@ -179,11 +178,11 @@ public function persist(Entity &$entity) if (Annotations::classHasAnnotation($entity, "@idGenerator")) { $idGeneratorAnnotation = Annotations::ofClass($entity, "@idGenerator"); - if (\is_subclass_of($idGeneratorAnnotation[0]->generator, IEntityIdGenerator::class)) { + if (\is_subclass_of($idGeneratorAnnotation[0]->generator, IPrimaryKeyGenerator::class)) { // We are safe ! // Generate an entity primary key using the generator $idGeneratorClass = new \ReflectionClass($idGeneratorAnnotation[0]->generator); - /** @var IEntityIdGenerator $idGenerator */ + /** @var IPrimaryKeyGenerator $idGenerator */ $idGenerator = $idGeneratorClass->newInstance(); $entity->{$idProperty} = $idGenerator->generate($entity); @@ -228,12 +227,12 @@ public function persist(Entity &$entity) /** * Merges the entity in the database with the given one. * - * @param Entity $entity The entity to edit. + * @param IEntity $entity The entity to edit. * * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException * @throws \ElementaryFramework\LightQL\Exceptions\EntityException */ - public function merge(Entity &$entity) + public function merge(IEntity &$entity) { $entityAnnotation = Annotations::ofClass($entity, "@entity"); @@ -287,12 +286,12 @@ public function merge(Entity &$entity) /** * Removes an entity from the database. * - * @param Entity $entity The entity to delete. + * @param IEntity $entity The entity to delete. * * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException * @throws \ElementaryFramework\LightQL\Exceptions\EntityException */ - public function delete(Entity &$entity) + public function delete(IEntity &$entity) { $entityAnnotation = Annotations::ofClass($entity, "@entity"); @@ -324,7 +323,7 @@ public function delete(Entity &$entity) foreach ($columns as $property => $column) { if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); $pk[] = $property; } } diff --git a/src/LightQL/Entities/IEntity.php b/src/LightQL/Entities/IEntity.php index 0ecbb72..de1758b 100644 --- a/src/LightQL/Entities/IEntity.php +++ b/src/LightQL/Entities/IEntity.php @@ -1,72 +1,43 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Entities; /** * IEntity * - * Provides default methods for all entities. - * - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/IEntity + * Provides a base interface for all entity implementations. It is not recommended to use this interface + * directly. Instead, you should use the {@see Entity} class as a base class for your entities. */ interface IEntity { - /** * Populates data in the entity. * * @param array $data The raw database data. */ - function hydrate(array $data); + public function hydrate(array $data): void; /** * Gets the raw value of a table column. * * @param string $column The table column name. - * - * @return mixed */ - function get(string $column); + public function get(string $column): mixed; /** * Sets the raw value of a table column. * * @param string $column The table column name. - * @param mixed $value The table column value. + * @param mixed $value The table column value. */ - function set(string $column, $value); + public function set(string $column, mixed $value): void; + /** + * Gets the entity columns. + * + * @return array + */ + public function getColumns(): array; } diff --git a/src/LightQL/Entities/IEntityIdGenerator.php b/src/LightQL/Entities/IEntityIdGenerator.php deleted file mode 100644 index 0a4a55a..0000000 --- a/src/LightQL/Entities/IEntityIdGenerator.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Entities; - -/** - * IEntityIdGenerator - * - * Defines a class as a primary key generator of a column. - * - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/IEntityIdGenerator - */ -interface IEntityIdGenerator -{ - /** - * Generates a new, unique primary key value for the given entity. - * - * @param Entity $entity The entity which we have to generate a key. - * - * @return null|int|string|IPrimaryKey - */ - function generate(Entity $entity); -} diff --git a/src/LightQL/Entities/IPrimaryKey.php b/src/LightQL/Entities/IPrimaryKey.php index c3a1817..07c706b 100644 --- a/src/LightQL/Entities/IPrimaryKey.php +++ b/src/LightQL/Entities/IPrimaryKey.php @@ -1,46 +1,14 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Entities; /** * IPrimaryKey * - * Defines a class as a primary key of an entity - * - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/IPrimaryKey + * This interface is used to create custom primary key types for entities. It is mainly combined with an implementation + * of the {@see IPrimaryKeyGenerator} interface. */ interface IPrimaryKey { diff --git a/src/LightQL/Entities/IPrimaryKeyGenerator.php b/src/LightQL/Entities/IPrimaryKeyGenerator.php new file mode 100644 index 0000000..6b4556f --- /dev/null +++ b/src/LightQL/Entities/IPrimaryKeyGenerator.php @@ -0,0 +1,22 @@ + - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -33,6 +33,8 @@ namespace ElementaryFramework\LightQL\Entities; use ElementaryFramework\LightQL\Exceptions\QueryException; +use ReflectionClass; +use ReflectionException; /** * Query @@ -41,7 +43,7 @@ * * @category Entities * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/Query */ class Query @@ -56,7 +58,7 @@ class Query /** * The reflection class of the managed entity. * - * @var \ReflectionClass + * @var ReflectionClass */ private $_entityReflection; @@ -94,11 +96,14 @@ public function __construct(EntityManager $manager) /** * Sets the reflection class of the managed entity. * - * @param \ReflectionClass $entity The managed entity reflection class instance. + * @param class-string|IEntity $entity The managed entity reflection class instance. + * @throws ReflectionException */ - public function setEntity(\ReflectionClass $entity) + public function setEntity(string|IEntity $entity): self { - $this->_entityReflection = $entity; + $this->_entityReflection = new ReflectionClass($entity); + + return $this; } /** @@ -106,9 +111,11 @@ public function setEntity(\ReflectionClass $entity) * * @param string $query The named query. */ - public function setQuery(string $query) + public function setQuery(string $query): self { $this->_namedQuery = $query; + + return $this; } /** @@ -117,9 +124,11 @@ public function setQuery(string $query) * @param string $name The name of the parameter in the query. * @param mixed $value The value of this parameter. */ - public function setParam(string $name, $value) + public function setParam(string $name, $value): self { $this->_parameters[$name] = $value; + + return $this; } /** diff --git a/src/LightQL/Entities/Relation.php b/src/LightQL/Entities/Relation.php new file mode 100644 index 0000000..7e11fa3 --- /dev/null +++ b/src/LightQL/Entities/Relation.php @@ -0,0 +1,20 @@ + = TEntity + */ +class Relation +{ + public function __construct( + public private(set) IEntity|array $related + ) + { + } +} \ No newline at end of file diff --git a/src/LightQL/Enums/DBMS.php b/src/LightQL/Enums/DBMS.php new file mode 100644 index 0000000..c2f4ece --- /dev/null +++ b/src/LightQL/Enums/DBMS.php @@ -0,0 +1,66 @@ + 'mysql', + self::POSTGRESQL => 'pgsql', + self::SQLITE => 'sqlite', + self::MSSQL => 'sqlsrv', + self::ORACLE => 'oci', + self::SYBASE => 'dblib', + self::CUSTOM => '', + }; + } +} diff --git a/src/LightQL/Enums/FetchMode.php b/src/LightQL/Enums/FetchMode.php new file mode 100644 index 0000000..58a0de2 --- /dev/null +++ b/src/LightQL/Enums/FetchMode.php @@ -0,0 +1,12 @@ + - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -37,7 +37,7 @@ * * @category Exceptions * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/EntityException */ class EntityException extends \Exception diff --git a/src/LightQL/Exceptions/FacadeException.php b/src/LightQL/Exceptions/FacadeException.php index 44c2faf..1f320be 100644 --- a/src/LightQL/Exceptions/FacadeException.php +++ b/src/LightQL/Exceptions/FacadeException.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -37,7 +37,7 @@ * * @category Exceptions * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/FacadeException */ class FacadeException extends \Exception diff --git a/src/LightQL/Exceptions/LightQLException.php b/src/LightQL/Exceptions/LightQLException.php index f4eebf9..b6223c2 100644 --- a/src/LightQL/Exceptions/LightQLException.php +++ b/src/LightQL/Exceptions/LightQLException.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -37,7 +37,7 @@ * * @category Exceptions * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/LightQLException */ class LightQLException extends \Exception diff --git a/src/LightQL/Exceptions/PersistenceUnitException.php b/src/LightQL/Exceptions/PersistenceUnitException.php index 22c92c8..5c0ab87 100644 --- a/src/LightQL/Exceptions/PersistenceUnitException.php +++ b/src/LightQL/Exceptions/PersistenceUnitException.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -37,7 +37,7 @@ * * @category Exceptions * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/PersistenceUnitException */ class PersistenceUnitException extends \Exception diff --git a/src/LightQL/Exceptions/QueryException.php b/src/LightQL/Exceptions/QueryException.php index 90e0996..24974ef 100644 --- a/src/LightQL/Exceptions/QueryException.php +++ b/src/LightQL/Exceptions/QueryException.php @@ -23,8 +23,8 @@ * * @category Library * @package LightQL - * @author Axel Nana - * @copyright 2018 Aliens Group, Inc. + * @author Axel Nana + * @copyright 2018 Aliens Group * @license MIT * @version 1.0.0 * @link http://lightql.na2axl.tk @@ -37,7 +37,7 @@ * * @category Exceptions * @package LightQL - * @author Nana Axel + * @author Nana Axel * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/QueryException */ class QueryException extends \Exception diff --git a/src/LightQL/LightQL.php b/src/LightQL/LightQL.php index 5f8b9fd..a5fcf4e 100644 --- a/src/LightQL/LightQL.php +++ b/src/LightQL/LightQL.php @@ -1,1060 +1,374 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL; -use ElementaryFramework\Annotations\Annotations; -use ElementaryFramework\LightQL\Annotations\AutoIncrementAnnotation; -use ElementaryFramework\LightQL\Annotations\ColumnAnnotation; -use ElementaryFramework\LightQL\Annotations\EntityAnnotation; -use ElementaryFramework\LightQL\Annotations\IdAnnotation; -use ElementaryFramework\LightQL\Annotations\IdGeneratorAnnotation; -use ElementaryFramework\LightQL\Annotations\ManyToManyAnnotation; -use ElementaryFramework\LightQL\Annotations\ManyToOneAnnotation; -use ElementaryFramework\LightQL\Annotations\NamedQueryAnnotation; -use ElementaryFramework\LightQL\Annotations\NotNullAnnotation; -use ElementaryFramework\LightQL\Annotations\OneToManyAnnotation; -use ElementaryFramework\LightQL\Annotations\OneToOneAnnotation; -use ElementaryFramework\LightQL\Annotations\PersistenceUnitAnnotation; -use ElementaryFramework\LightQL\Annotations\SizeAnnotation; -use ElementaryFramework\LightQL\Annotations\UniqueAnnotation; +use ElementaryFramework\LightQL\Enums\DBMS; use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Query\Builder; +use PDO; +use PDOException; +use PDOStatement; /** - * LightQL - Database Manager Class - * - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/LightQL + * Query builder class with parameter binding and type safety. */ -class LightQL +final class LightQL { - /** - * Registered SQL operators. - * - * @var array - * @access private - */ - private static $_operators = array('!=', '<>', '<=', '>=', '=', '<', '>'); - - /** - * Register all annotations in the manager - */ - public static function registerAnnotations() - { - $manager = Annotations::getManager(); - - $manager->registerAnnotation("autoIncrement", AutoIncrementAnnotation::class); - $manager->registerAnnotation("column", ColumnAnnotation::class); - $manager->registerAnnotation("entity", EntityAnnotation::class); - $manager->registerAnnotation("id", IdAnnotation::class); - $manager->registerAnnotation("idGenerator", IdGeneratorAnnotation::class); - $manager->registerAnnotation("manyToMany", ManyToManyAnnotation::class); - $manager->registerAnnotation("manyToOne", ManyToOneAnnotation::class); - $manager->registerAnnotation("namedQuery", NamedQueryAnnotation::class); - $manager->registerAnnotation("notNull", NotNullAnnotation::class); - $manager->registerAnnotation("oneToMany", OneToManyAnnotation::class); - $manager->registerAnnotation("oneToOne", OneToOneAnnotation::class); - $manager->registerAnnotation("persistenceUnit", PersistenceUnitAnnotation::class); - $manager->registerAnnotation("size", SizeAnnotation::class); - $manager->registerAnnotation("unique", UniqueAnnotation::class); - } - - /** - * The database name. - * - * @var string - * @access protected - */ - protected $database; - - /** - * The table name. - * - * @var string - * @access protected - */ - protected $table; - - /** - * The database server address. - * - * @var string - * @access protected - */ - protected $hostname; - - /** - * The database username. - * - * @var string - * @access protected - */ - protected $username; - - /** - * The database password. - * - * @var string - * @access protected - */ - protected $password; - - /** - * The PDO driver to use. - * - * @var string - * @access private - */ - private $_driver; - - /** - * The DBMS to use. - * - * @var string - * @access private - */ - private $_dbms; - - /** - * The PDO connection options. - * - * @var array - * @access private - */ - private $_options; - - /** - * The DSN used for the PDO connection. - * - * @var string - * @access private - */ - private $_dsn; - /** * The current PDO instance. - * - * @var \PDO - * @access private */ - private $_pdo = null; + public private(set) ?PDO $pdo = null; /** - * The where clause. + * Class constructor. * - * @var string - * @access private + * @param string $database + * @param string $hostname + * @param string $username + * @param string $password + * @param DBMS $dbms + * @param array $pdoOptions + * @param int|null $port + * @param string|null $socket + * @param string|null $charset + * @param string|null $customDsn + * @throws LightQLException */ - private $_where = null; + public function __construct( + private readonly string $database, + private readonly string $hostname = '', + private readonly string $username = '', + private readonly string $password = '', + public private(set) readonly DBMS $dbms = DBMS::SQLITE, + private readonly array $pdoOptions = [], + private readonly ?int $port = null, + private readonly ?string $socket = null, + private readonly ?string $charset = null, + ?string $customDsn = null, + ) { + $dsn = $customDsn ?? $this->buildDsn(); + $commands = $this->getInitCommands(); - /** - * The order clause. - * - * @var string - * @access private - */ - private $_order = null; - - /** - * The limit clause. - * - * @var string - * @access private - */ - private $_limit = null; - - /** - * The "group by" clause - * - * @var string - * @access private - */ - private $_group = null; - - /** - * The distinct clause - * - * @var bool - * @access private - */ - private $_distinct = false; - - /** - * The computed query string. - * - * @var string - * @access private - */ - private $_queryString = null; + $this->connect($dsn, $commands); + } /** - * Class __constructor - * - * @param array $options The lists of options + * Factory method for backward compatibility with array options. * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @param array $options + * @throws LightQLException */ - public function __construct(array $options = null) + public static function fromArray(array $options): self { - if (!is_array($options)) { - return false; - } - - $attr = array(); - - if (isset($options["dbms"])) { - $this->_dbms = strtolower($options["dbms"]); - } - - if (isset($options["options"])) { - $this->_options = $options["options"]; - } - - if (isset($options["command"]) && is_array($options["command"])) { - $commands = $options["command"]; - } else { - $commands = []; - } - - if (isset($options["dsn"])) { - if (is_array($options["dsn"]) && isset($options["dsn"]["driver"])) { - $this->_driver = $options["dsn"]["driver"]; - unset($options["dsn"]["driver"]); - $attr = $options["dsn"]; - } else { - return false; - } - } else { - if (isset($options["port"]) && is_int($options["port"] * 1)) { - $port = $options["port"]; - } - - switch ($this->_dbms) { - case "mariadb": - case "mysql": - $this->_driver = "mysql"; - $attr = array( - "dbname" => $options["database"] - ); - - if (isset($options["socket"])) { - $attr["unix_socket"] = $options["socket"]; - } else { - $attr["host"] = $options["hostname"]; - if (isset($port)) { - $attr["port"] = $port; - } - } - - // Make MySQL using standard quoted identifier - $commands[] = "SET SQL_MODE=ANSI_QUOTES"; - break; - - case "pgsql": - $this->_driver = "pgsql"; - $attr = array( - "host" => $options["hostname"], - "dbname" => $options['database'] - ); - - if (isset($port)) { - $attr["port"] = $port; - } - break; - - case "sybase": - $this->_driver = "dblib"; - $attr = array( - "host" => $options["hostname"], - "dbname" => $options["database"] - ); - - if (isset($port)) { - $attr["port"] = $port; - } - break; - - case "oracle": - $this->_driver = "oci"; - $attr = array( - "dbname" => $options["hostname"] ? - "//{$options['server']}" . (isset($port) ? ":{$port}" : ":1521") . "/{$options['database']}" : - $options['database'] - ); - - if (isset($options["charset"])) { - $attr["charset"] = $options["charset"]; - } - break; - - case "mssql": - if (isset($options["driver"]) && $options["driver"] === "dblib") { - $this->_driver = "dblib"; - $attr = array( - "host" => $options["hostname"] . (isset($port) ? ":{$port}" : ""), - "dbname" => $options["database"] - ); - } else { - $this->_driver = "sqlsrv"; - $attr = array( - "Server" => $options["hostname"] . (isset($port) ? ",{$port}" : ""), - "Database" => $options["database"] - ); - } + $dbms = DBMS::tryFrom(strtolower($options['dbms'] ?? 'sqlite')) ?? DBMS::SQLITE; - // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting - $commands[] = "SET QUOTED_IDENTIFIER ON"; - // Make ANSI_NULLS is ON for NULL value - $commands[] = "SET ANSI_NULLS ON"; - break; + if (isset($options['dsn']['driver'])) { + $driver = $options['dsn']['driver']; + unset($options['dsn']['driver']); - case "sqlite": - $this->_driver = "sqlite"; - $attr = array( - $options['database'] - ); - break; + $dsnParts = []; + foreach ($options['dsn'] as $key => $value) { + $dsnParts[] = is_int($key) ? $value : "{$key}={$value}"; } + $customDsn = $driver . ':' . implode(';', $dsnParts); + + return new self( + database: $options['database'] ?? '', + hostname: $options['hostname'] ?? '', + username: $options['username'] ?? '', + password: $options['password'] ?? '', + dbms: $dbms, + pdoOptions: $options['options'] ?? [], + customDsn: $customDsn, + ); } - $stack = []; - foreach ($attr as $key => $value) { - $stack[] = is_int($key) ? $value : "{$key}={$value}"; - } - - $this->_dsn = $this->_driver . ":" . implode($stack, ";"); - - if (in_array($this->_dbms, ['mariadb', 'mysql', 'pgsql', 'sybase', 'mssql']) && isset($options['charset'])) { - $commands[] = "SET NAMES '{$options['charset']}'"; - } - - $this->hostname = $options["hostname"]; - $this->database = $options["database"]; - $this->username = isset($options['username']) ? $options['username'] : null; - $this->password = isset($options['password']) ? $options['password'] : null; - - $this->_instantiate(); - - foreach ($commands as $value) { - $this->_pdo->exec($value); - } - - return $this; + return new self( + database: $options['database'] ?? '', + hostname: $options['hostname'] ?? '', + username: $options['username'] ?? '', + password: $options['password'] ?? '', + dbms: $dbms, + pdoOptions: $options['options'] ?? [], + port: isset($options['port']) && is_numeric($options['port']) ? (int)$options['port'] : null, + socket: $options['socket'] ?? null, + charset: $options['charset'] ?? null, + ); } /** - * Closes a connection - * - * @return void + * Close the database connection. */ public function close(): void { - $this->_pdo = false; - } - - /** - * Connect to the database / Instantiate PDO - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException When the connexion fails. - * - * @return void - */ - private function _instantiate(): void - { - try { - $this->_pdo = new \PDO( - $this->_dsn, - $this->username, - $this->password, - $this->_options - ); - } catch (\PDOException $e) { - throw new LightQLException($e->getMessage()); - } - } - - /** - * Gets the current query string. - * - * @return string - */ - public function getQueryString(): string - { - return $this->_queryString; + $this->pdo = null; } /** - * Changes the currently used table - * - * @param string $table The table's name - * - * @return \ElementaryFramework\LightQL\LightQL + * Create a new query builder instance. */ - public function from(string $table): LightQL + public function builder(): Builder { - $this->table = $table; - return $this; + return new Builder($this); } /** - * Add a where condition. - * - * @param string|array $condition SQL condition in valid format + * Execute raw query. * - * @return \ElementaryFramework\LightQL\LightQL + * @throws LightQLException */ - public function where($condition): LightQL + public function query(string $query, int $mode = PDO::FETCH_ASSOC): PDOStatement { - // where(array('field1'=>'value', 'field2'=>'value')) - $this->_where = (null !== $this->_where) ? "{$this->_where} OR (" : "("; - if (is_array($condition)) { - $i = 0; - $operand = "="; - foreach ($condition as $column => $value) { - $this->_where .= ($i > 0) ? " AND " : ""; - if (is_int($column)) { - $this->_where .= $value; - } else { - $parts = explode(" ", $this->parseValue($value)); - foreach (self::$_operators as $operator) { - if (in_array($operator, $parts, true) && $parts[0] === $operator) { - $operand = $operator; - } - } - $this->_where .= "{$column} {$operand} " . str_replace($operand, "", $value); - $operand = "="; - } - ++$i; + try { + $statement = $this->pdo->query($query, $mode); + if ($statement === false) { + throw new LightQLException("Query execution failed"); } - } else { - $this->_where .= $condition; + return $statement; + } catch (PDOException $e) { + throw new LightQLException("Query failed: " . $e->getMessage()); } - $this->_where .= ")"; - - return $this; } /** - * Add an order clause. - * - * @param string $column The column to sort. - * @param string $mode The sort mode. - * - * @return \ElementaryFramework\LightQL\LightQL - */ - public function order(string $column, string $mode = "ASC"): LightQL - { - $this->_order = " ORDER BY {$column} {$mode} "; - return $this; - } - - /** - * Add a limit clause. - * - * @param int $offset The limit offset. - * @param int $count The number of elements after the offset. - * - * @return \ElementaryFramework\LightQL\LightQL - */ - public function limit(int $offset, int $count): LightQL - { - $this->_limit = " LIMIT {$offset}, {$count} "; - return $this; - } - - /** - * Add a group clause. - * - * @param string $column The column used to group results. + * Prepare a statement. * - * @return \ElementaryFramework\LightQL\LightQL + * @param array $options + * @throws LightQLException */ - public function groupBy(string $column): LightQL + public function prepare(string $query, array $options = []): PDOStatement { - $this->_group = $column; - return $this; - } - - /** - * Add a distinct clause. - * - * @return \ElementaryFramework\LightQL\LightQL - */ - public function distinct(): LightQL - { - $this->_distinct = true; - return $this; - } - - /** - * Selects data in database. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return \PDOStatement - */ - public function select($columns = "*"): \PDOStatement - { - return $this->_select($columns); - } - - /** - * Executes the SELECT SQL query. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return \PDOStatement - */ - private function _select($columns): \PDOStatement - { - // Constructing the fields list - if (is_array($columns)) { - $_fields = ""; - - foreach ($columns as $column => $alias) { - if (is_int($column)) { - $_fields .= "{$alias}, "; - } elseif (is_string($column)) { - $_fields .= "{$column} AS {$alias}, "; - } + try { + $statement = $this->pdo->prepare($query, $options); + if ($statement === false) { + throw new LightQLException("Failed to prepare statement"); } - - $columns = trim($_fields, ", "); - } elseif (!is_string($columns)) { - throw new LightQLException( - "Invalid data given for the parameter \$columns." . - " Only string and array are supported." - ); - } - - // Constructing the SELECT query string - $this->_queryString = trim("SELECT" . (($this->_distinct) ? " DISTINCT " : " ") . "{$columns} FROM {$this->table}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : " ") . ((null !== $this->_order) ? $this->_order : " ") . ((null !== $this->_limit) ? $this->_limit : " ") . ((null !== $this->_group) ? "GROUP BY {$this->_group}" : " ")); - - // Preparing the query - $getFieldsData = $this->prepare($this->_queryString); - - // Executing the query - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - - return $getFieldsData; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); + return $statement; + } catch (PDOException $e) { + throw new LightQLException("Prepare failed: " . $e->getMessage()); } } /** - * Prepares a query. - * - * @param string $query The query to execute - * @param array $options PDO options - * - * @uses \PDO::prepare() - * - * @return \PDOStatement + * Get last insert ID. */ - public function prepare(string $query, array $options = array()): \PDOStatement + public function lastInsertId(): int { - return $this->_pdo->prepare($query, $options); + return (int)$this->pdo->lastInsertId(); } /** - * Reset all clauses. + * Quote a value. */ - protected function resetClauses() + public function quote(string $value): string { - $this->_distinct = false; - $this->_where = null; - $this->_order = null; - $this->_limit = null; - $this->_group = null; + return $this->pdo->quote($value); } /** - * Selects the first data result of the query. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return array + * Begin transaction. */ - public function selectFirst($columns = "*") + public function beginTransaction(): bool { - $result = $this->selectArray($columns); - - if (count($result) > 0) { - return $result[0]; - } - - return null; + return $this->pdo->beginTransaction(); } /** - * Selects data as array of arrays in database. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return array + * Commit transaction. */ - public function selectArray($columns = "*"): array + public function commit(): bool { - $select = $this->_select($columns); - $result = array(); - - while ($r = $select->fetch(\PDO::FETCH_LAZY)) { - $result[] = array_diff_key((array)$r, array("queryString" => "queryString")); - } - - return $result; + return $this->pdo->commit(); } /** - * Selects data as array of objects in database. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return array + * Rollback transaction. */ - public function selectObject($columns = "*"): array + public function rollback(): bool { - $select = $this->_select($columns); - $result = array(); - - while ($r = $select->fetch(\PDO::FETCH_OBJ)) { - $result[] = $r; - } - - return $result; + return $this->pdo->rollBack(); } /** - * Selects data in database with table joining. + * Gets the error message from the last query, if any. * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * @param mixed $params The information used for JOIN. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return \PDOStatement + * @return string|null The error message from the last query, or null if no error occurred. */ - public function join($columns, $params): \PDOStatement + public function lastError(): ?string { - return $this->_join($columns, $params); + return $this->pdo->errorInfo()[2] ?? null; } /** - * Executes a SELECT ... JOIN query. - * - * @param string|array $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * @param string|array $params The information used for JOIN. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return \PDOStatement + * Build DSN string based on a DBMS type. + * @throws LightQLException */ - private function _join($columns, $params): \PDOStatement + private function buildDsn(): string { - $joints = $params; + $driver = $this->dbms->getDriver(); - if (is_array($columns)) { - $columns = implode(",", $columns); - } - - if (is_array($params)) { - $joints = ""; + $attr = match ($this->dbms) { + DBMS::MYSQL, DBMS::MARIADB => $this->buildMySqlDsnAttributes(), + DBMS::POSTGRESQL => $this->buildPgSqlDsnAttributes(), + DBMS::SYBASE => $this->buildSybaseDsnAttributes(), + DBMS::ORACLE => $this->buildOracleDsnAttributes(), + DBMS::MSSQL => $this->buildMsSqlDsnAttributes($driver), + DBMS::SQLITE => [$this->database], + DBMS::CUSTOM => throw new LightQLException("Custom DSN is required when using DBMS::CUSTOM"), + }; - foreach ($params as $param) { - if (is_array($param)) { - $joints .= " {$param['side']} JOIN {$param['table']} ON {$param['cond']} "; - } elseif (is_string($param)) { - $joints .= " {$param} "; - } else { - throw new LightQLException("Invalid value used for join."); - } - } + $parts = []; + foreach ($attr as $key => $value) { + $parts[] = is_int($key) ? $value : "{$key}={$value}"; } - $this->_queryString = trim("SELECT" . (($this->_distinct) ? " DISTINCT " : " ") . "{$columns} FROM {$this->table} {$joints}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : " ") . ((null !== $this->_order) ? $this->_order : " ") . ((null !== $this->_limit) ? $this->_limit : "")); - - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return $getFieldsData; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } + return $driver . ':' . implode(';', $parts); } /** - * Selects data as array of arrays in database with table joining. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * @param mixed $params The information used for JOIN. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return array + * @return array */ - public function joinArray($columns, $params): array + private function buildMySqlDsnAttributes(): array { - $join = $this->_join($columns, $params); - $result = array(); + $attr = ['dbname' => $this->database]; - while ($r = $join->fetch(\PDO::FETCH_LAZY)) { - $result[] = array_diff_key((array)$r, array("queryString" => "queryString")); + if ($this->socket !== null) { + $attr['unix_socket'] = $this->socket; + } else { + $attr['host'] = $this->hostname; + if ($this->port !== null) { + $attr['port'] = $this->port; + } } - return $result; + return $attr; } /** - * Selects data as array of objects in database with table joining. - * - * @param mixed $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * @param mixed $params The information used for JOIN. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return array + * @return array */ - public function joinObject($columns, $params): array + private function buildPgSqlDsnAttributes(): array { - $join = $this->_join($columns, $params); - $result = array(); + $attr = [ + 'host' => $this->hostname, + 'dbname' => $this->database, + ]; - while ($r = $join->fetch(\PDO::FETCH_OBJ)) { - $result[] = $r; + if ($this->port !== null) { + $attr['port'] = $this->port; } - return $result; + return $attr; } /** - * Counts data in table. - * - * @param string|array $columns The fields to select. This value can be an array of fields, - * or a string of fields (according to the SELECT SQL query syntax). - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return int|array + * @return array */ - public function count($columns = "*") + private function buildSybaseDsnAttributes(): array { - if (is_array($columns)) { - $column = implode(",", $columns); - } - - $this->_queryString = trim("SELECT" . ((null !== $this->_group) ? "{$this->_group}," : " ") . "COUNT(" . ((isset($column)) ? $column : $columns) . ") AS lightql_count FROM {$this->table}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : " ") . ((null !== $this->_limit) ? $this->_limit : " ") . ((null !== $this->_group) ? "GROUP BY {$this->_group}" : " ")); + $attr = [ + 'host' => $this->hostname, + 'dbname' => $this->database, + ]; - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - if (null === $this->_group) { - $this->resetClauses(); - $data = $getFieldsData->fetch(); - return (int) $data['lightql_count']; - } - - $this->resetClauses(); - $res = array(); - - while ($data = $getFieldsData->fetch()) { - $res[$data[$this->_group]] = $data['lightql_count']; - } - - return $res; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } - } - - /** - * Inserts one set of data in table. - * - * @param array $fieldsAndValues The fields and the associated values to insert. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return boolean - */ - public function insert(array $fieldsAndValues): bool - { - $columns = array(); - $values = array(); - - foreach ($fieldsAndValues as $column => $value) { - $columns[] = $column; - $values[] = $this->parseValue($value); + if ($this->port !== null) { + $attr['port'] = $this->port; } - $column = implode(",", $columns); - $value = implode(",", $values); - - $this->_queryString = trim("INSERT INTO {$this->table}({$column}) VALUE ({$value})"); - - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } + return $attr; } /** - * Inserts a multiple set of data at once in table. - * - * @param array $columns The list of fields to use. - * @param array $values The array of list of values to insert - * into the specified fields. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return boolean + * @return array */ - public function insertMany(array $columns, array $values): bool + private function buildOracleDsnAttributes(): array { - $column = implode(",", $columns); + $dbname = $this->hostname + ? "//{$this->hostname}" . ($this->port ? ":{$this->port}" : ':1521') . "/{$this->database}" + : $this->database; - $this->_queryString = "INSERT INTO {$this->table}({$column}) VALUES"; + $attr = ['dbname' => $dbname]; - foreach ($values as $i => $value) { - $value = implode(",", array_map(array($this, "parseValue"), $value)); - $this->_queryString .= ($i === 0 ? "" : ", ") . " ({$value})"; + if ($this->charset !== null) { + $attr['charset'] = $this->charset; } - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } + return $attr; } /** - * Updates data in table. - * - * @param array $fieldsAndValues The fields and the associated values to update. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return boolean + * @return array */ - public function update(array $fieldsAndValues): bool + private function buildMsSqlDsnAttributes(string &$driver): array { - $updates = ""; - $count = count($fieldsAndValues); + // MSSQL can use dblib or sqlsrv driver + $useDblib = false; // Can be configured if needed - if (is_array($fieldsAndValues)) { - foreach ($fieldsAndValues as $column => $value) { - $count--; - $updates .= "{$column} = " . $this->parseValue($value); - $updates .= ($count != 0) ? ", " : ""; - } - } else { - $updates = $fieldsAndValues; + if ($useDblib) { + $driver = 'dblib'; + return [ + 'host' => $this->hostname . ($this->port ? ":{$this->port}" : ''), + 'dbname' => $this->database, + ]; } - $this->_queryString = trim("UPDATE {$this->table} SET {$updates}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : "")); - - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } + $driver = 'sqlsrv'; + return [ + 'Server' => $this->hostname . ($this->port ? ",{$this->port}" : ''), + 'Database' => $this->database, + ]; } /** - * Deletes data in table. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * Get initialization commands for the connection. * - * @return boolean + * @return array */ - public function delete(): bool + private function getInitCommands(): array { - $this->_queryString = trim("DELETE FROM {$this->table}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : "")); + $commands = []; - $getFieldsData = $this->prepare($this->_queryString); + $commands = match ($this->dbms) { + DBMS::MYSQL, DBMS::MARIADB => ['SET SQL_MODE=ANSI_QUOTES'], + DBMS::MSSQL => ['SET QUOTED_IDENTIFIER ON', 'SET ANSI_NULLS ON'], + default => [], + }; - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); + if ( + in_array($this->dbms, [DBMS::MYSQL, DBMS::MARIADB, DBMS::POSTGRESQL, DBMS::SYBASE, DBMS::MSSQL]) + && $this->charset !== null + ) { + $commands[] = "SET NAMES " . $this->quote($this->charset); } - } - /** - * Truncates a table. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return boolean - */ - public function truncate(): bool - { - $this->_queryString = "TRUNCATE {$this->table}"; - - $getFieldsData = $this->prepare($this->_queryString); - - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); - } - } - - /** - * Executes a query. - * - * @param string $query The query to execute - * @param int $mode The fetch mode - * - * @uses \PDO::query() - * - * @return \PDOStatement - */ - public function query(string $query, int $mode = \PDO::FETCH_LAZY): \PDOStatement - { - return $this->_pdo->query($query, $mode); + return $commands; } /** - * Gets the last inserted id by an - * INSERT query. - * - * @uses \PDO::lastInsertId() + * Establish database connection. * - * @return int + * @param array $initCommands + * @throws LightQLException */ - public function lastInsertID(): int + private function connect(string $dsn, array $initCommands): void { - return intval($this->_pdo->lastInsertId()); - } - - /** - * Quotes a value. - * - * @param mixed $value The value to quote. - * - * @uses \PDO::quote() - * - * @return string - */ - public function quote($value): string - { - return $this->_pdo->quote($value); - } - - /** - * Disable auto commit mode and start a transaction. - * - * @uses \PDO::beginTransaction() - * - * @return bool - */ - public function beginTransaction(): bool - { - return $this->_pdo->beginTransaction(); - } - - /** - * Commit changes made during a transaction. - * - * @uses \PDO::commit() - * - * @return bool - */ - public function commit(): bool - { - return $this->_pdo->commit(); - } + try { + $this->pdo = new PDO( + $dsn, + $this->username, + $this->password, + $this->pdoOptions + ); - /** - * Rollback changes made during a transaction. - * - * @uses \PDO::rollBack() - * - * @return bool - */ - public function rollback(): bool - { - return $this->_pdo->rollBack(); - } + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - /** - * Converts a value to a string. - * - * @param mixed $value The value to convert. - * - * @return string - */ - public function parseValue($value): string - { - if (is_null($value)) { - return "NULL"; - } elseif (is_bool($value)) { - return $value ? "1" : "0"; - } else { - return strval($value); + foreach ($initCommands as $command) { + $this->pdo->exec($command); + } + } catch (PDOException $e) { + throw new LightQLException("Database connection failed: " . $e->getMessage()); } } } diff --git a/src/LightQL/Persistence/PersistenceUnit.php b/src/LightQL/Persistence/PersistenceUnit.php index 383063e..d47aa55 100644 --- a/src/LightQL/Persistence/PersistenceUnit.php +++ b/src/LightQL/Persistence/PersistenceUnit.php @@ -1,105 +1,63 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Persistence; +use ElementaryFramework\LightQL\Enums\DBMS; use ElementaryFramework\LightQL\Exceptions\PersistenceUnitException; /** * Persistence Unit * * Configures parameters to use for database connection. - * - * @category Persistence - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Persistence/PersistenceUnit */ class PersistenceUnit { /** * The DBMS. - * - * @var string */ - private $_dbms; + private(set) DBMS $dbms; /** * The database server address. - * - * @var string */ - private $_hostname; + private(set) string $hostname; /** * The database name. - * - * @var string */ - private $_database; + private(set) string $database; /** * The username to use on connection. - * - * @var string */ - private $_username; + private(set) string $username; /** * The password associated to the username. - * - * @var string */ - private $_password; + private(set) string $password; /** * The list of registered persistence unit files. * - * @var array + * @var array */ - private static $_registry = array(); + private static array $_registry = array(); /** - * @var PersistenceUnit[] + * @var list */ - private static $_units = array(); + private static array $_units = array(); /** * Registers a new persistence unit. * - * @param string $key The name of the persistence unit. + * @param string $key The name of the persistence unit. * @param string $path The path to the persistence unit file. */ - public static function register(string $key, string $path) + public static function register(string $key, string $path): void { self::$_registry[$key] = $path; } @@ -107,10 +65,10 @@ public static function register(string $key, string $path) /** * Cleans the persistence unit registry and cache. */ - public static function purge() + public static function purge(): void { - self::$_registry = array(); - self::$_units = array(); + self::$_registry = []; + self::$_units = []; } /** @@ -137,31 +95,31 @@ private function __construct(string $key) } if (array_key_exists("DBMS", $content)) { - $this->_dbms = $content["DBMS"]; + $this->dbms = DBMS::from($content["DBMS"]); } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the DBMS value."); } if (array_key_exists("Hostname", $content)) { - $this->_hostname = $content["Hostname"]; + $this->hostname = $content["Hostname"]; } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the Hostname value."); } if (array_key_exists("DatabaseName", $content)) { - $this->_database = $content["DatabaseName"]; + $this->database = $content["DatabaseName"]; } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the DatabaseName value."); } if (array_key_exists("Username", $content)) { - $this->_username = $content["Username"]; + $this->username = $content["Username"]; } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the Username value."); } if (array_key_exists("Password", $content)) { - $this->_password = $content["Password"]; + $this->password = $content["Password"]; } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the Password value."); } @@ -174,65 +132,14 @@ private function __construct(string $key) * Creates a new persistence unit by the given key. * * @param string $key The persistence unit name. - * * @return PersistenceUnit - */ - public static function create(string $key) - { - if (array_key_exists($key, self::$_units)) { - return self::$_units[$key]; - } else { - return (self::$_units[$key] = new self($key)); - } - } - - /** - * Returns the DBMS. * - * @return string - */ - public function getDbms() - { - return $this->_dbms; - } - - /** - * Returns the database name. - * - * @return string - */ - public function getDatabase(): string - { - return $this->_database; - } - - /** - * Returns the database server name. - * - * @return string - */ - public function getHostname(): string - { - return $this->_hostname; - } - - /** - * Returns the password of the user. - * - * @return string - */ - public function getPassword(): string - { - return $this->_password; - } - - /** - * Returns the username. - * - * @return string + * @throws PersistenceUnitException */ - public function getUsername(): string + public static function create(string $key): static { - return $this->_username; + return array_key_exists($key, self::$_units) + ? self::$_units[$key] + : ((self::$_units[$key] = new static($key))); } } diff --git a/src/LightQL/Query/Builder.php b/src/LightQL/Query/Builder.php new file mode 100644 index 0000000..a36b834 --- /dev/null +++ b/src/LightQL/Query/Builder.php @@ -0,0 +1,805 @@ + + */ + private const array OPERATORS = ['!=', '<>', '<=', '>=', '=', '<', '>', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN']; + + /** + * The table name. + */ + private string $table = ''; + + /** + * Where conditions with parameters. + * + * @var array}> + */ + private array $whereConditions = []; + + /** + * Order by clauses. + * + * @var array + */ + private array $orderClauses = []; + + /** + * Offset value for the LIMIT clause. + */ + private ?int $limitOffset = null; + + /** + * Count value for the LIMIT clause. + */ + private ?int $limitCount = null; + + /** + * Group by columns. + * + * @var array + */ + private array $groupByColumns = []; + + /** + * Having clause with parameters. + */ + private ?string $havingClause = null; + + /** + * @var array + */ + private array $havingParams = []; + + /** + * Distinct flag. + */ + private bool $distinct = false; + + /** + * Join clauses. + * + * @var array + */ + private array $joinClauses = []; + + /** + * Parameter counter for unique parameter names. + */ + private int $paramCounter = 0; + + /** + * Create a new builder instance. + * + * @param LightQL $lightQL The LightQL instance. + */ + public function __construct( + private readonly LightQL $lightQL, + ) {} + + /** + * Set the table to query. + * + * @param string $table The table name. + */ + public function from(string $table): self + { + $this->table = $table; + return $this; + } + + /** + * Add WHERE conditions to the query. + * + * The `$condition` parameter can be a raw SQL condition or an associative array of column names and values. + * If the value is an array, it will be treated as an IN condition. + * + * The array form of the condition allows specifying the operator using the column name as a key. + * i.e., `$query->where(['age >=' => 18, 'status' => 'active'])` + * + * @param array|string $condition The WHERE condition. + * @param array $params The parameters to bind. Used for raw SQL conditions. + */ + public function where(array|string $condition, array $params = []): self + { + if (is_string($condition)) { + $this->whereConditions[] = [ + 'sql' => $condition, + 'params' => $params, + ]; + return $this; + } + + $whereParts = []; + $whereParams = []; + + foreach ($condition as $column => $value) { + if (is_int($column)) { + // Raw SQL condition + $whereParts[] = $value; + continue; + } + + // Extract operator from the column name if present + $operator = '='; + $cleanColumn = $column; + + foreach (self::OPERATORS as $op) { + if (str_ends_with($column, ' ' . $op)) { + $operator = $op; + $cleanColumn = substr($column, 0, - (strlen($op) + 1)); + break; + } + } + + $paramName = $this->generateParamName($cleanColumn); + + if (is_array($value)) { + // Handle IN clause + $placeholders = []; + foreach ($value as $i => $val) { + $inParamName = "{$paramName}_{$i}"; + $placeholders[] = ":{$inParamName}"; + $whereParams[$inParamName] = $val; + } + $whereParts[] = "{$cleanColumn} IN (" . implode(', ', $placeholders) . ")"; + } else { + $whereParts[] = "{$cleanColumn} {$operator} :{$paramName}"; + $whereParams[$paramName] = $value; + } + } + + if (!empty($whereParts)) { + $this->whereConditions[] = [ + 'sql' => '(' . implode(' AND ', $whereParts) . ')', + 'params' => $whereParams, + ]; + } + + return $this; + } + + /** + * Add an ORDER BY clause. + * + * @param string $column The column to order by. + * @param SortOrder $order The sort order. + */ + public function orderBy(string $column, SortOrder $order = SortOrder::ASC): self + { + $this->orderClauses[] = "{$column} {$order->value}"; + return $this; + } + + /** + * Add a LIMIT clause. + * + * @param int $offset The limit offset. + * @param int $count The limit count. + */ + public function limit(int $offset, int $count): self + { + $this->limitOffset = $offset; + $this->limitCount = $count; + return $this; + } + + /** + * Add a GROUP BY clause. + * + * @param string $column The column to group by. + */ + public function groupBy(string $column): self + { + $this->groupByColumns[] = $column; + return $this; + } + + /** + * Add a HAVING clause. + * + * @param string $condition The condition to filter by. + * @param array $params The parameters to bind in the condition. + */ + public function having(string $condition, array $params = []): self + { + $this->havingClause = $condition; + $this->havingParams = $params; + return $this; + } + + /** + * Add a DISTINCT clause. + */ + public function distinct(): self + { + $this->distinct = true; + return $this; + } + + /** + * Add an INNER JOIN clause. + * + * @param string $table The table to join. + * @param string $on The ON condition for the join. + * @return Builder + */ + public function innerJoin(string $table, string $on): self + { + $this->joinClauses[] = [ + 'type' => JoinType::INNER, + 'table' => $table, + 'on' => $on + ]; + return $this; + } + + /** + * Add a LEFT JOIN clause. + * + * @param string $table The table to join. + * @param string $on The ON condition for the join. + * @return Builder + */ + public function leftJoin(string $table, string $on): self + { + $this->joinClauses[] = [ + 'type' => JoinType::LEFT, + 'table' => $table, + 'on' => $on + ]; + return $this; + } + + /** + * Add a RIGHT JOIN clause. + * + * @param string $table The table to join. + * @param string $on The ON condition for the join. + * @return Builder + */ + public function rightJoin(string $table, string $on): self + { + $this->joinClauses[] = [ + 'type' => JoinType::RIGHT, + 'table' => $table, + 'on' => $on + ]; + return $this; + } + + /** + * Add a FULL JOIN clause. + * + * @param string $table The table to join. + * @param string $on The ON condition for the join. + * @return Builder + */ + public function fullJoin(string $table, string $on): self + { + $this->joinClauses[] = [ + 'type' => JoinType::FULL, + 'table' => $table, + 'on' => $on + ]; + return $this; + } + + /** + * Add a CROSS JOIN clause. + * + * @param string $table The table to join. + * @return Builder + */ + public function crossJoin(string $table): self + { + $this->joinClauses[] = [ + 'type' => JoinType::CROSS, + 'table' => $table, + 'on' => null + ]; + return $this; + } + + /** + * Add a JOIN clause (defaults to INNER JOIN). + * + * @param string $table The table to join. + * @param string $on The ON condition for the join. + * @return Builder + */ + public function join(string $table, string $on): self + { + return $this->innerJoin($table, $on); + } + + /** + * Build a SELECT query and return a pending query. + * + * @param array|string $columns The columns to select. + * @throws LightQLException + */ + public function select(array|string $columns = '*'): PendingQuery + { + $result = $this->buildSelectQuery($columns); + $pendingQuery = new PendingQuery($result['query'], $result['params'], $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Count rows. + * + * @param array|string $columns + * @return int|array + * @throws LightQLException|QueryException + */ + public function count(array|string $columns = '*'): int|array + { + $this->validateTable(); + + $columnList = is_array($columns) ? implode(', ', $columns) : $columns; + $boundParams = []; + + $groupByClause = $this->buildGroupByClause(); + $selectColumns = !empty($this->groupByColumns) + ? implode(', ', $this->groupByColumns) . ', ' + : ''; + + $query = "SELECT {$selectColumns}COUNT({$columnList}) AS lightql_count FROM {$this->table}"; + $query .= $this->buildJoinClause(); + $query .= $this->buildWhereClause($boundParams); + $query .= $this->buildLimitClause(); + $query .= $groupByClause; + + $pendingQuery = new PendingQuery(trim($query), $boundParams, $this->lightQL); + $hasGroupBy = !empty($this->groupByColumns); + $firstGroupColumn = $this->groupByColumns[0] ?? null; + $this->reset(); + + $statement = $pendingQuery->execute(); + + if (!$hasGroupBy) { + $result = $statement->fetchFirst(); + return (int)$result['lightql_count']; + } + + $results = []; + while ($row = $statement->fetchFirst()) { + $key = $row[$firstGroupColumn]; + $results[$key] = (int)$row['lightql_count']; + } + + return $results; + } + + /** + * Insert a single row with parameter binding. + * + * @param array $fieldsAndValues + * @throws LightQLException + */ + public function insert(array $fieldsAndValues): PendingQuery + { + $result = $this->buildInsertQuery($fieldsAndValues); + $pendingQuery = new PendingQuery($result['query'], $result['params'], $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Insert multiple rows with parameter binding. + * + * @param array> $entries The array of items to insert into the table. + * @param list $columns The list of columns to insert. + * + * @throws LightQLException + */ + public function insertMany(array $entries, array $columns = []): PendingQuery + { + $this->validateTable(); + + if (empty($entries)) { + throw new LightQLException("Cannot insert empty data"); + } + + if (empty($columns)) { + $columns = array_keys($entries[0]); + } + + $columnList = implode(', ', $columns); + $valueSets = []; + $params = []; + + foreach ($entries as $rowIndex => $row) { + $placeholders = []; + foreach ($row as $column => $value) { + if (!in_array($column, $columns, true)) { + continue; + } + + $paramName = "{$column}_{$rowIndex}"; + $placeholders[] = ":{$paramName}"; + $params[$paramName] = $value; + } + $valueSets[] = '(' . implode(', ', $placeholders) . ')'; + } + + $query = "INSERT INTO {$this->table} ({$columnList}) VALUES " . implode(', ', $valueSets); + + $pendingQuery = new PendingQuery($query, $params, $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Update rows with parameter binding. + * + * @param array|string $fieldsAndValues + * @throws LightQLException + */ + public function update(array|string $fieldsAndValues): PendingQuery + { + $result = $this->buildUpdateQuery($fieldsAndValues); + $pendingQuery = new PendingQuery($result['query'], $result['params'], $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Delete rows. + * + * @throws LightQLException + */ + public function delete(): PendingQuery + { + $result = $this->buildDeleteQuery(); + $pendingQuery = new PendingQuery($result['query'], $result['params'], $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Truncate table. + * + * @throws LightQLException + */ + public function truncate(): PendingQuery + { + $result = $this->buildTruncateQuery(); + $pendingQuery = new PendingQuery($result['query'], $result['params'], $this->lightQL); + $this->reset(); + return $pendingQuery; + } + + /** + * Reset query builder state. + */ + public function reset(): self + { + $this->whereConditions = []; + $this->orderClauses = []; + $this->limitOffset = null; + $this->limitCount = null; + $this->groupByColumns = []; + $this->havingClause = null; + $this->havingParams = []; + $this->distinct = false; + $this->joinClauses = []; + $this->paramCounter = 0; + + return $this; + } + + /** + * Build SELECT query string. + * + * @param array|string $columns + * @return array{query: string, params: array} + * @throws LightQLException + */ + private function buildSelectQuery(array|string $columns): array + { + $this->validateTable(); + + $columnList = $this->buildColumnList($columns); + $boundParams = []; + + $query = 'SELECT' . ($this->distinct ? ' DISTINCT' : '') . " {$columnList} FROM {$this->table}"; + $query .= $this->buildJoinClause(); + $query .= $this->buildWhereClause($boundParams); + $query .= $this->buildGroupByClause(); + $query .= $this->buildHavingClause($boundParams); + $query .= $this->buildOrderByClause(); + $query .= $this->buildLimitClause(); + + return [ + 'query' => trim($query), + 'params' => $boundParams, + ]; + } + + /** + * Build INSERT query string. + * + * @param array $fieldsAndValues + * @return array{query: string, params: array} + * @throws LightQLException + */ + private function buildInsertQuery(array $fieldsAndValues): array + { + $this->validateTable(); + + if (empty($fieldsAndValues)) { + throw new LightQLException("Cannot insert empty data"); + } + + $columns = array_keys($fieldsAndValues); + $placeholders = []; + $params = []; + + foreach ($fieldsAndValues as $column => $value) { + $paramName = $this->generateParamName($column); + $placeholders[] = ":{$paramName}"; + $params[$paramName] = $value; + } + + $columnList = implode(', ', $columns); + $placeholderList = implode(', ', $placeholders); + + $query = "INSERT INTO {$this->table} ({$columnList}) VALUES ({$placeholderList})"; + + return [ + 'query' => $query, + 'params' => $params, + ]; + } + + /** + * Build UPDATE query string. + * + * @param array|string $fieldsAndValues + * @return array{query: string, params: array} + * @throws LightQLException + */ + private function buildUpdateQuery(array|string $fieldsAndValues): array + { + $this->validateTable(); + + if (is_string($fieldsAndValues)) { + $setClause = $fieldsAndValues; + $params = []; + } else { + if (empty($fieldsAndValues)) { + throw new LightQLException("Cannot update with empty data"); + } + + $setParts = []; + $params = []; + + foreach ($fieldsAndValues as $column => $value) { + $paramName = $this->generateParamName($column); + $setParts[] = "{$column} = :{$paramName}"; + $params[$paramName] = $value; + } + + $setClause = implode(', ', $setParts); + } + + $boundParams = []; + $query = "UPDATE {$this->table} SET {$setClause}"; + $query .= $this->buildWhereClause($boundParams); + + $allParams = array_merge($params, $boundParams); + + return [ + 'query' => trim($query), + 'params' => $allParams, + ]; + } + + /** + * Build DELETE query string. + * + * @return array{query: string, params: array} + * @throws LightQLException + */ + private function buildDeleteQuery(): array + { + $this->validateTable(); + + $boundParams = []; + $query = "DELETE FROM {$this->table}"; + $query .= $this->buildWhereClause($boundParams); + + return [ + 'query' => trim($query), + 'params' => $boundParams, + ]; + } + + /** + * Build TRUNCATE query string. + * + * @return array{query: string, params: array} + * @throws LightQLException + */ + private function buildTruncateQuery(): array + { + $this->validateTable(); + + $query = match ($this->lightQL->dbms) { + DBMS::SQLITE => "DELETE FROM {$this->table}", + default => "TRUNCATE TABLE {$this->table}", + }; + + return [ + 'query' => $query, + 'params' => [], + ]; + } + + /** + * Build a column list from array or string. + * + * @param array|string $columns + */ + private function buildColumnList(array|string $columns): string + { + if (is_string($columns)) { + return $columns; + } + + $parts = []; + foreach ($columns as $column => $alias) { + if (is_int($column)) { + $parts[] = $alias; + } else { + $parts[] = "{$column} AS {$alias}"; + } + } + + return implode(', ', $parts); + } + + /** + * Build WHERE clause from conditions. + * + * @param array $boundParams Reference to params array to populate + */ + private function buildWhereClause(array &$boundParams): string + { + if (empty($this->whereConditions)) { + return ''; + } + + $parts = []; + foreach ($this->whereConditions as $condition) { + $parts[] = $condition['sql']; + $boundParams = array_merge($boundParams, $condition['params']); + } + + return ' WHERE ' . implode(' OR ', $parts); + } + + /** + * Build ORDER BY clause. + */ + private function buildOrderByClause(): string + { + if (empty($this->orderClauses)) { + return ''; + } + + return ' ORDER BY ' . implode(', ', $this->orderClauses); + } + + /** + * Build LIMIT clause. + */ + private function buildLimitClause(): string + { + if ($this->limitOffset === null || $this->limitCount === null) { + return ''; + } + + return match ($this->lightQL->dbms) { + DBMS::POSTGRESQL => " LIMIT {$this->limitCount} OFFSET {$this->limitOffset}", + DBMS::MSSQL => " OFFSET {$this->limitOffset} ROWS FETCH NEXT {$this->limitCount} ROWS ONLY", + default => " LIMIT {$this->limitOffset}, {$this->limitCount}", + }; + } + + /** + * Build GROUP BY clause. + */ + private function buildGroupByClause(): string + { + if (empty($this->groupByColumns)) { + return ''; + } + + return ' GROUP BY ' . implode(', ', $this->groupByColumns); + } + + /** + * Build HAVING clause. + * + * @param array $boundParams Reference to a params array to populate + */ + private function buildHavingClause(array &$boundParams): string + { + if ($this->havingClause === null) { + return ''; + } + + $boundParams = array_merge($boundParams, $this->havingParams); + return ' HAVING ' . $this->havingClause; + } + + /** + * Build JOIN clause from stored join clauses. + * + * @return string + */ + private function buildJoinClause(): string + { + if (empty($this->joinClauses)) { + return ''; + } + + $parts = []; + foreach ($this->joinClauses as $join) { + if ($join['type'] === JoinType::CROSS) { + $parts[] = " {$join['type']->value} JOIN {$join['table']}"; + } else { + $parts[] = " {$join['type']->value} JOIN {$join['table']} ON {$join['on']}"; + } + } + + return implode('', $parts); + } + + /** + * Generate unique parameter name. + * + * @param string $column The name of the column from which the parameter is generated. + * @return string The generated parameter name. + */ + private function generateParamName(string $column): string + { + $safeName = preg_replace('/[^a-zA-Z0-9_]/', '_', $column); + return $safeName . '_' . ($this->paramCounter++); + } + + /** + * Validate that a table is set. + * + * @throws LightQLException + */ + private function validateTable(): void + { + if (empty($this->table)) { + throw new LightQLException("No table specified. Use from() method first."); + } + } +} diff --git a/src/LightQL/Query/PendingQuery.php b/src/LightQL/Query/PendingQuery.php new file mode 100644 index 0000000..f77c20f --- /dev/null +++ b/src/LightQL/Query/PendingQuery.php @@ -0,0 +1,111 @@ + $parameters The bound parameters + * @param LightQL $lightQL The LightQL instance for execution + */ + public function __construct( + private string $queryString, + private array $parameters, + private LightQL $lightQL, + ) {} + + /** + * Execute the query and return the QueryResult. + * + * @throws QueryException|LightQLException + */ + public function execute(): QueryResult + { + try { + $statement = $this->lightQL->prepare($this->queryString); + + foreach ($this->parameters as $param => $value) { + $type = match (true) { + is_int($value) => PDO::PARAM_INT, + is_bool($value) => PDO::PARAM_BOOL, + is_null($value) => PDO::PARAM_NULL, + default => PDO::PARAM_STR, + }; + + $statement->bindValue(":{$param}", $value, $type); + } + + $statement->execute(); + + return new QueryResult($statement, $this->queryString, $this->lightQL); + } catch (PDOException $e) { + throw new QueryException( + "Query execution failed: " . $e->getMessage() . " | Query: {$this->queryString}" + ); + } + } + + /** + * Get the query string. + */ + public function getQueryString(): string + { + return $this->queryString; + } + + /** + * Get the query string (magic method). + */ + public function __toString(): string + { + return $this->queryString; + } + + /** + * Get the value of a specific parameter. + * + * @param string $name The parameter name. + * + * @return mixed + */ + public function getParameter(string $name): mixed + { + return $this->parameters[$name] ?? null; + } + + /** + * Get all parameters. + * + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Check if a parameter exists. + * + * @param string $name The parameter name. + */ + public function hasParameter(string $name): bool + { + return array_key_exists($name, $this->parameters); + } +} diff --git a/src/LightQL/Query/QueryResult.php b/src/LightQL/Query/QueryResult.php new file mode 100644 index 0000000..b75aa67 --- /dev/null +++ b/src/LightQL/Query/QueryResult.php @@ -0,0 +1,148 @@ +statement->columnCount() > 0; + } + + /** + * Fetch all rows as an associative array. + * + * @return array> + * @throws QueryException + */ + public function fetchAll(): array + { + $this->ensureHasResults(); + + return $this->statement->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Generator that yields rows one by one as associative arrays. + * Useful for processing large result sets without loading everything into memory. + * + * @return Generator> + * @throws QueryException + */ + public function rows(): Generator + { + $this->ensureHasResults(); + + while ($row = $this->statement->fetch(PDO::FETCH_ASSOC)) { + yield $row; + } + } + + /** + * Fetch the first row as an associative array. + * + * @return array|null + * @throws QueryException + */ + public function fetchFirst(): ?array + { + $this->ensureHasResults(); + + $result = $this->statement->fetch(PDO::FETCH_ASSOC); + return $result !== false ? $result : null; + } + + /** + * Fetch a single column value from the first row. + * + * @param int|string $column Column index (0-based) or column name + * @return mixed + * @throws QueryException + */ + public function fetchColumn(int|string $column = 0): mixed + { + $this->ensureHasResults(); + + if (is_string($column)) { + $row = $this->statement->fetch(PDO::FETCH_ASSOC); + return $row !== false ? ($row[$column] ?? null) : null; + } + + return $this->statement->fetchColumn($column); + } + + /** + * Get the number of rows affected by the last DELETE, INSERT, or UPDATE statement. + */ + public function getRowCount(): int + { + return $this->statement->rowCount(); + } + + /** + * Get the number of columns in the result set. + */ + public function getColumnCount(): int + { + return $this->statement->columnCount(); + } + + /** + * Get the last insert ID (useful for INSERT queries with auto-increment columns). + */ + public function getLastInsertId(): int + { + return $this->lightQL->lastInsertId(); + } + + /** + * Get column metadata. + * + * @param int $column 0-based column index + * @return array|false + */ + public function getColumnMeta(int $column): array|false + { + return $this->statement->getColumnMeta($column); + } + + /** + * Ensure that the result set has results. + * + * @throws QueryException + */ + private function ensureHasResults(): void + { + if (!$this->hasResults()) { + throw new QueryException("Cannot perform operation on a query that returns no data."); + } + } +} diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index d607a68..7777f8d 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -1,282 +1,206 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Sessions; -use ElementaryFramework\Annotations\Annotations; -use ElementaryFramework\Annotations\Exceptions\AnnotationException; - -use ElementaryFramework\LightQL\Annotations\NamedQueryAnnotation; -use ElementaryFramework\LightQL\Entities\Entity; +use ElementaryFramework\LightQL\Annotations\Column; +use ElementaryFramework\LightQL\Annotations\ManyToMany; +use ElementaryFramework\LightQL\Annotations\ManyToOne; +use ElementaryFramework\LightQL\Annotations\NamedQuery; +use ElementaryFramework\LightQL\Annotations\OneToMany; +use ElementaryFramework\LightQL\Annotations\OneToOne; +use ElementaryFramework\LightQL\Annotations\PersistenceUnit as PersistenceUnitAnnotation; +use ElementaryFramework\LightQL\Annotations\Table; use ElementaryFramework\LightQL\Entities\EntityManager; use ElementaryFramework\LightQL\Entities\IEntity; +use ElementaryFramework\LightQL\Entities\IPrimaryKey; use ElementaryFramework\LightQL\Entities\Query; +use ElementaryFramework\LightQL\Entities\Relation; +use ElementaryFramework\LightQL\Enums\FetchMode; use ElementaryFramework\LightQL\Exceptions\EntityException; use ElementaryFramework\LightQL\Exceptions\FacadeException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Exceptions\PersistenceUnitException; +use ElementaryFramework\LightQL\Exceptions\QueryException; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; +use ReflectionAttribute; +use ReflectionClass; +use ReflectionException; +use ReflectionProperty; /** - * Facade + * Base class for entity facades. * - * Base class for all entity facades. + * @template TEntity of IEntity * - * @abstract - * @category Sessions - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Sessions/Facade + * @implements IFacade */ abstract class Facade implements IFacade { /** * The entity manager of this facade. - * - * @var EntityManager */ - protected $entityManager; + protected EntityManager $entityManager; /** * The entity class name managed by this facade. - * - * @var \ReflectionClass */ - private $_class; + private Table $entityTableAnnotation; + + /** + * @var class-string The entity class managed by this facade. + */ + private string $entityClass; /** * Facade constructor. * - * @param string $class The entity class name managed by this facade. + * @param class-string|TEntity $class The entity class name managed by this facade. * - * @throws EntityException When the entity class or object doesn't have an @entity annotation. - * @throws FacadeException When the "entityManager" property of this Facade doesn't have a @persistenceUnit annotation. + * @throws FacadeException When the entity class or object doesn't have a Table attribute. + * @throws FacadeException When the "entityManager" property of this Facade doesn't have a PersistenceUnit attribute. * @throws FacadeException When the entity class or object doesn't inherit from the Entity class. - * @throws AnnotationException When the Facade is unable to read an annotation. + * @throws ReflectionException + * @throws LightQLException + * @throws PersistenceUnitException */ - public function __construct($class) + public function __construct(string|IEntity $class) { - if (!Annotations::propertyHasAnnotation($this, "entityManager", "@persistenceUnit")) { - throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no @persistenceUnit annotation."); + $propAttributes = new ReflectionProperty($this, 'entityManager') + ->getAttributes(PersistenceUnitAnnotation::class); + + if (empty($propAttributes)) { + throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no PersistenceUnit attribute."); } - if (!Annotations::classHasAnnotation($class, "@entity")) { - throw new EntityException("Cannot create an entity without the @entity annotation."); + $reflection = new ReflectionClass($class); + $classAttributes = $reflection->getAttributes(Table::class); + + if (empty($classAttributes)) { + throw new FacadeException("Cannot create the entity facade. The managed entity is missing the Table attribute."); } - if (!is_subclass_of($class, Entity::class)) { - throw new FacadeException("Unable to create a facade. The entity class or object seems to be invalid."); + if (!is_subclass_of($class, IEntity::class)) { + throw new FacadeException("Unable to create a facade. The provided entity class doesn't inherit from the Entity class."); } - $this->_class = new \ReflectionClass($class); + $this->entityClass = $reflection->getName(); + + /** @var Table $entityTableAnnotation */ + $entityTableAnnotation = $classAttributes[0]->newInstance(); + $this->entityTableAnnotation = $entityTableAnnotation; - $annotations = Annotations::ofProperty($this, "entityManager", "@persistenceUnit"); - $this->entityManager = new EntityManager(PersistenceUnit::create($annotations[0]->name)); + /** @var PersistenceUnitAnnotation $persistenceUnit */ + $persistenceUnit = $propAttributes[0]->newInstance(); + $this->entityManager = new EntityManager(PersistenceUnit::create($persistenceUnit->name)); } /** - * Creates an entity. - * - * @param Entity $entity The entity to create. - * - * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * {@inheritdoc} */ - public function create(Entity &$entity) + public function create(IEntity &$entity): void { - if (!$this->_class->isInstance($entity)) { - throw new FacadeException("Cannot create entity. The type of the entity is not valid for this facade."); - } - - try { - $this->entityManager->persist($entity); - - $columns = $entity->getColumns(); - foreach ($columns as $property => $column) { - if ($column->isOneToMany) { - $this->_fetchOneToMany($entity, $property); - } elseif ($column->isManyToOne) { - $this->_fetchManyToOne($entity, $property); - } elseif ($column->isManyToMany) { - $this->_fetchManyToMany($entity, $property); - } elseif ($column->isOneToOne) { - $this->_fetchOneToOne($entity, $property); - } + $this->ensureEntityType($entity); + + $this->entityManager->persist($entity); + + $columns = $entity->getColumns(); + foreach ($columns as $property => $column) { + if ($column->isOneToMany) { + $this->handleRelation($entity, $property, $this->fetchOneToMany(...)); + } elseif ($column->isManyToOne) { + $this->handleRelation($entity, $property, $this->fetchManyToOne(...)); + } elseif ($column->isManyToMany) { + $this->handleRelation($entity, $property, $this->fetchManyToMany(...)); + } elseif ($column->isOneToOne) { + $this->handleRelation($entity, $property, $this->fetchOneToOne(...)); } - } catch (\Exception $e) { - throw new FacadeException($e->getMessage()); } } /** - * Edit an entity. - * - * @param Entity $entity The entity to edit. - * - * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * {@inheritdoc} */ - public function edit(Entity &$entity) + public function update(IEntity &$entity): void { - if (!$this->_class->isInstance($entity)) { - throw new FacadeException("Cannot edit entity. The type of the entity is not valid for this facade."); - } + $this->ensureEntityType($entity); $this->entityManager->merge($entity); } /** - * Delete an entity. - * - * @param Entity $entity The entity to delete. - * - * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * {@inheritdoc } */ - public function delete(Entity &$entity) + public function delete(IEntity &$entity): void { - if (!$this->_class->isInstance($entity)) { - throw new FacadeException("Cannot edit entity. The type of the entity is not valid for this facade."); - } + $this->ensureEntityType($entity); $this->entityManager->merge($entity); $this->entityManager->delete($entity); } /** - * Find an entity. + * {@inheritdoc} * - * @param mixed $id The id of the entity to find - * - * @return Entity - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException */ - public function find($id): Entity + public function retrieve(int|string|IPrimaryKey $id): IEntity { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - - return $this->_parseRawEntity( - $this->entityManager->find($this->getEntityClassName(), $id), - $annotations + return $this->parseRawEntity( + $this->entityManager->find($this->entityClass, $id), ); } /** - * Find all entities. + * {@inheritdoc} * - * @return Entity[] - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException */ - public function findAll(): array + public function list(): array { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - $rawEntities = $this->entityManager ->getLightQL() - ->from($annotations[0]->table) - ->selectArray(); + ->builder() + ->from($this->entityTableAnnotation->table) + ->select() + ->execute() + ->fetchAll(); - return $this->_parseRawEntities($rawEntities, $annotations); + return $this->parseRawEntities($rawEntities); } /** - * Find all entities in the given range. + * {@inheritdoc} * - * @param int $start The starting offset. - * @param int $length The number of entities to find. - * - * @return Entity[] - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException */ - public function findRange(int $start, int $length): array + public function range(int $start, int $length): array { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - $rawEntities = $this->entityManager ->getLightQL() - ->from($annotations[0]->table) + ->builder() + ->from($this->entityTableAnnotation->table) ->limit($start, $length) - ->selectArray(); + ->select() + ->execute() + ->fetchAll(); - return $this->_parseRawEntities($rawEntities, $annotations); + return $this->parseRawEntities($rawEntities); } /** - * Count the number of entities. - * - * @return int - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * {@inheritdoc} */ public function count(): int { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - return $this->entityManager ->getLightQL() - ->from($annotations[0]->table) + ->builder() + ->from($this->entityTableAnnotation->table) ->count(); } - /** - * Returns the entity class name of this facade. - * - * @return string - */ - public function getEntityClassName(): string - { - return $this->_class->getName(); - } - - /** - * Returns the entity manager for this facade. - * - * @return EntityManager - */ - public function getEntityManager(): EntityManager - { - return $this->entityManager; - } - /** * Get a named query. * @@ -285,19 +209,21 @@ public function getEntityManager(): EntityManager * @return Query * * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException + * @throws ReflectionException */ public function getNamedQuery(string $name): Query { - if (!Annotations::classHasAnnotation($this->_class->name, "@namedQuery")) { - throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation."); + $reflection = new ReflectionClass($this->entityClass); + $attributes = array_map(fn(ReflectionAttribute $attribute) => $attribute->newInstance(), $reflection->getAttributes(NamedQuery::class)); + + if (empty($attributes)) { + throw new FacadeException("The {$reflection->name} class has no NamedQuery attribute."); } - $namedQueries = Annotations::ofClass($this->getEntityClassName(), "@namedQuery"); $query = null; - /** @var NamedQueryAnnotation $namedQuery */ - foreach ($namedQueries as $namedQuery) { + /** @var NamedQuery $namedQuery */ + foreach ($attributes as $namedQuery) { if ($namedQuery->name === $name) { $query = $namedQuery->query; break; @@ -305,39 +231,44 @@ public function getNamedQuery(string $name): Query } if ($query === null) { - throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation with the name {$name}."); + throw new FacadeException("The {$reflection->name} class has no NamedQuery attribute with the name {$name}."); } - $q = new Query($this->entityManager); - $q->setEntity($this->_class); - $q->setQuery($query); - - return $q; + return new Query($this->entityManager) + ->setEntity($this->entityClass) + ->setQuery($query); } /** * Fetch data for a many-to-many relation. * - * @param IEntity $entity The managed entity. - * @param string $property The property in many-to-many relation. + * @param TEntity $entity The managed entity. + * @param string $property The property in many-to-many relation. + * @return list * * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException + * @throws ReflectionException + * @throws QueryException */ - private function _fetchManyToMany(&$entity, $property) + private function fetchManyToMany(IEntity $entity, string $property): array { - $manyToMany = Annotations::ofProperty($entity, $property, "@manyToMany"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $entityAnnotations = Annotations::ofClass($entity, "@entity"); + $propReflection = new ReflectionProperty($entity, $property); + + /** @var ManyToMany $manyToMany */ + $manyToMany = $propReflection->getAttributes(ManyToMany::class)[0]->newInstance(); + /** @var Column $column */ + $column = $propReflection->getAttributes(Column::class)[0]->newInstance(); $mappedPropertyName = null; - $referencedEntity = new $manyToMany[0]->entity; + /** @var IEntity $referencedEntity */ + $referencedEntity = new $manyToMany->entity; foreach ($referencedEntity->getColumns() as $p => $c) { if ($c->isManyToMany) { - $mappedManyToMany = Annotations::ofProperty($referencedEntity, $p, "@manyToMany"); - if ($mappedManyToMany[0]->crossTable === $manyToMany[0]->crossTable) { + /** @var ManyToMany $mappedManyToMany */ + $mappedManyToMany = new ReflectionProperty($referencedEntity, $p)->getAttributes(ManyToMany::class)[0]->newInstance(); + if ($mappedManyToMany->pivotTable === $manyToMany->pivotTable) { $mappedPropertyName = $p; break; } @@ -346,160 +277,182 @@ private function _fetchManyToMany(&$entity, $property) unset($referencedEntity); if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with a @manyToMany annotation in the entity \"$manyToMany[0]->entity\"."); + throw new EntityException("Unable to find a suitable property with a ManyToMany attribute in the entity \"{$manyToMany->entity}\"."); } - $mappedPropertyManyToManyAnnotation = Annotations::ofProperty($manyToMany[0]->entity, $mappedPropertyName, "@manyToMany"); - $mappedPropertyColumnAnnotation = Annotations::ofProperty($manyToMany[0]->entity, $mappedPropertyName, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($manyToMany[0]->entity, "@entity"); - - $lightql = $this->entityManager->getLightQL(); - - $results = $lightql - ->from($manyToMany[0]->crossTable) - ->where(array("{$manyToMany[0]->crossTable}.{$manyToMany[0]->referencedColumn}" => $lightql->quote($entity->get($column[0]->name)))) - ->joinArray( - "{$referencedEntityAnnotations[0]->table}.*", - array( - array( - "side" => "LEFT", - "table" => $referencedEntityAnnotations[0]->table, - "cond" => "{$manyToMany[0]->crossTable}.{$mappedPropertyAnnotation[0]->referencedColumn} = {$referencedEntityAnnotations[0]->table}.{$mappedPropertyColumnAnnotation[0]->name}" - ) + /** @var ManyToMany $mappedPropertyManyToManyAnnotation */ + $mappedPropertyManyToManyAnnotation = new ReflectionProperty($manyToMany->entity, $mappedPropertyName)->getAttributes(ManyToMany::class)[0]->newInstance(); + /** @var Column $mappedPropertyColumnAnnotation */ + $mappedPropertyColumnAnnotation = new ReflectionProperty($manyToMany->entity, $mappedPropertyName)->getAttributes(Column::class)[0]->newInstance(); + /** @var Table $referencedEntityAnnotation */ + $referencedEntityAnnotation = new ReflectionClass($manyToMany->entity)->getAttributes(Table::class)[0]->newInstance(); + + $className = $manyToMany->entity; + + return array_map( + fn($item) => new $className($item), + $this->entityManager + ->getLightQL() + ->builder() + ->from($manyToMany->pivotTable) + ->where([ + "{$manyToMany->pivotTable}.{$manyToMany->foreignColumn}" => $entity->get($column->name) + ]) + ->leftJoin( + $referencedEntityAnnotation->table, + "{$manyToMany->pivotTable}.{$mappedPropertyManyToManyAnnotation->foreignColumn} = {$referencedEntityAnnotation->table}.{$mappedPropertyColumnAnnotation->name}" ) - ); - - $className = $manyToMany[0]->entity; - $entity->{$property} = array_map(function($item) use ($manyToMany, $className) { - return new $className($item); - }, $results); + ->select() + ->execute() + ->fetchAll() + ); } /** * Fetch data for a one-to-many relation. * - * @param IEntity $entity The managed entity. - * @param string $property The property in one-to-many relation. + * @param TEntity $entity The managed entity. + * @param string $property The property in one-to-many relation. + * @return TEntity|null * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException + * @throws ReflectionException + * @throws QueryException */ - private function _fetchOneToMany(&$entity, $property) + private function fetchOneToMany(IEntity $entity, string $property): IEntity|null { - $oneToMany = Annotations::ofProperty($entity, $property, "@oneToMany"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($oneToMany[0]->entity, "@entity"); - - $mappedPropertyName = $this->_resolveMappedPropertyName($oneToMany[0]->entity, "ManyToOne", $oneToMany[0]->referencedColumn); - - if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with @manyToOne annotation in the entity \"{$oneToMany[0]->entity}\"."); - } + /** @var OneToMany $oneToMany */ + $oneToMany = new ReflectionProperty($entity, $property)->getAttributes(OneToMany::class)[0]->newInstance(); + /** @var Column $column */ + $column = new ReflectionProperty($entity, $property)->getAttributes(Column::class)[0]->newInstance(); + /** @var Table $referencedEntityAnnotation */ + $referencedEntityAnnotation = new ReflectionClass($oneToMany->entity)->getAttributes(Table::class)[0]->newInstance(); - $mappedPropertyManyToOneAnnotation = Annotations::ofProperty($oneToMany[0]->entity, $mappedPropertyName, "@manyToOne"); + $mappedPropertyName = $oneToMany->mappedBy; - $lightql = $this->entityManager->getLightQL(); + /** @var ManyToOne $mappedPropertyManyToOneAnnotation */ + $mappedPropertyManyToOneAnnotation = new ReflectionProperty($oneToMany->entity, $mappedPropertyName)->getAttributes(ManyToOne::class)[0]->newInstance(); - $result = $lightql - ->from($referencedEntityAnnotations[0]->table) - ->where(array("{$referencedEntityAnnotations[0]->table}.{$oneToMany[0]->referencedColumn}" => $lightql->quote($entity->get($column[0]->name)))) - ->selectFirst("{$referencedEntityAnnotations[0]->table}.*"); - - $className = $oneToMany[0]->entity; - - $entity->{$property} = $result; - - if ($result !== null) { - $entity->{$property} = new $className($result); - } + $result = $this->entityManager + ->getLightQL() + ->builder() + ->from($referencedEntityAnnotation->table) + ->where([ + "{$referencedEntityAnnotation->table}.{$mappedPropertyManyToOneAnnotation->localColumn}" => $entity->get($column->name) + ]) + ->select("{$referencedEntityAnnotation->table}.*") + ->execute() + ->fetchFirst(); + + return $result !== null ? new ($oneToMany->entity)($result) : null; } /** * Fetch data for a many-to-one relation. * - * @param IEntity $entity The managed entity. - * @param string $property The property in many-to-one relation. + * @param TEntity $entity The managed entity. + * @param string $property The property in many-to-one relation. + * @return list * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException + * @throws EntityException + * @throws ReflectionException + * @throws QueryException */ - private function _fetchManyToOne(&$entity, $property) + private function fetchManyToOne(IEntity &$entity, string $property): array { - $manyToOne = Annotations::ofProperty($entity, $property, "@manyToOne"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($manyToOne[0]->entity, "@entity"); + /** @var ManyToOne $manyToOne */ + $manyToOne = new ReflectionProperty($entity, $property)->getAttributes(ManyToOne::class)[0]->newInstance(); + /** @var Column $column */ + $column = new ReflectionProperty($entity, $property)->getAttributes(Column::class)[0]->newInstance(); + /** @var Table $referencedEntityAnnotation */ + $referencedEntityAnnotation = new ReflectionClass($manyToOne->entity)->getAttributes(Table::class)[0]->newInstance(); - $mappedPropertyName = $this->_resolveMappedPropertyName($manyToOne[0]->entity, "OneToMany", $manyToOne[0]->referencedColumn); + $mappedPropertyName = $this->resolveMappedPropertyName($manyToOne->entity, "OneToMany", $manyToOne->referencedColumn); if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with @oneToMany annotation in the entity \"{$manyToOne[0]->entity}\"."); + throw new EntityException("Unable to find a suitable property with @oneToMany annotation in the entity \"{$manyToOne->entity}\"."); } - $lightql = $this->entityManager->getLightQL(); - - $results = $lightql - ->from($referencedEntityAnnotations[0]->table) - ->where(array("{$referencedEntityAnnotations[0]->table}.{$manyToOne[0]->referencedColumn}" => $lightql->quote($entity->get($column[0]->name)))) - ->selectArray("{$referencedEntityAnnotations[0]->table}.*"); - - $entity->{$property} = array_map(function($item) use ($manyToOne, $entity, $mappedPropertyName) { - $className = $manyToOne[0]->entity; - $e = new $className($item); - $e->{$mappedPropertyName} = &$entity; - return $e; - }, $results); + $className = $manyToOne->entity; + + return array_map( + function ($item) use ($className, &$entity, $mappedPropertyName) { + $e = new $className($item); + $e->{$mappedPropertyName} = &$entity; + return $e; + }, + $this->entityManager + ->getLightQL() + ->builder() + ->from($referencedEntityAnnotation->table) + ->where([ + "{$referencedEntityAnnotation->table}.{$manyToOne->referencedColumn}" => $entity->get($column->name) + ]) + ->select("{$referencedEntityAnnotation->table}.*") + ->execute() + ->fetchAll() + ); } /** * Fetch data for a one-to-one relation. * - * @param IEntity $entity The managed entity. - * @param string $property The property in one-to-one relation. + * @param TEntity $entity The managed entity. + * @param string $property The property in one-to-one relation. + * @return TEntity|null * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException + * @throws ReflectionException + * @throws EntityException + * @throws QueryException */ - private function _fetchOneToOne(&$entity, $property) + private function fetchOneToOne(IEntity &$entity, string $property): IEntity|null { - $oneToOne = Annotations::ofProperty($entity, $property, "@oneToOne"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($oneToOne[0]->entity, "@entity"); + /** @var OneToOne $oneToOne */ + $oneToOne = new ReflectionProperty($entity, $property)->getAttributes(OneToOne::class)[0]->newInstance(); + /** @var Column $column */ + $column = new ReflectionProperty($entity, $property)->getAttributes(Column::class)[0]->newInstance(); + /** @var Table $referencedEntityAnnotation */ + $referencedEntityAnnotation = new ReflectionClass($oneToOne->entity)->getAttributes(Table::class)[0]->newInstance(); - $mappedPropertyName = $this->_resolveMappedPropertyName($oneToOne[0]->entity, "OneToOne", $oneToOne[0]->referencedColumn); + $mappedPropertyName = $oneToOne->mappedBy ?? $this->resolveMappedPropertyName($oneToOne->entity, "OneToOne", $oneToOne->referencedColumn); if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with @oneToOne annotation in the entity \"{$oneToOne[0]->entity}\"."); + throw new EntityException("Unable to find a suitable property with OneToOne attribute in the entity \"{$oneToOne->entity}\"."); } - $mappedPropertyAnnotation = Annotations::ofProperty($oneToOne[0]->entity, $mappedPropertyName, "@oneToOne"); - - $lightql = $this->entityManager->getLightQL(); - - $result = $lightql - ->from($referencedEntityAnnotations[0]->table) - ->where(array("{$referencedEntityAnnotations[0]->table}.{$oneToOne[0]->referencedColumn}" => $lightql->quote($entity->get($column[0]->name)))) - ->selectFirst("{$referencedEntityAnnotations[0]->table}.*"); - - $className = $oneToOne[0]->entity; + $result = $this->entityManager + ->getLightQL() + ->builder() + ->from($referencedEntityAnnotation->table) + ->where([ + "{$referencedEntityAnnotation->table}.{$oneToOne->referencedColumn}" => $entity->get($column->name) + ]) + ->select("{$referencedEntityAnnotation->table}.*") + ->execute() + ->fetchFirst(); - $entity->{$property} = $result; if ($result !== null) { - $entity->{$property} = new $className($result); - $entity->{$property}->{$mappedPropertyName} = &$entity; + $relation = new ($oneToOne->entity)($result); + $relation->{$mappedPropertyName} = &$entity; + return $relation; } + + return null; } /** * Resolve the name of a property mapped by an annotation. * * @param string $entityClass The class name of the mapped property. - * @param string $check The type of annotation to find. - * @param string $column The mapped column name. + * @param string $check The type of annotation to find. + * @param string $column The mapped column name. * * @return string|null */ - private function _resolveMappedPropertyName(string $entityClass, string $check, string $column): string + private function resolveMappedPropertyName(string $entityClass, string $check, string $column): string|null { $mappedPropertyName = null; @@ -518,59 +471,84 @@ private function _resolveMappedPropertyName(string $entityClass, string $check, /** * Parse a set of raw data to a set of Entities. * - * @param array $rawEntities The set of raw entities data provided fromm database. - * @param array $annotations The set of entity annotations. + * @param list> $rawEntities The set of raw entities data provided from the database. * - * @return Entity[] + * @return list * * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException */ - private function _parseRawEntities($rawEntities, $annotations): array + private function parseRawEntities(array $rawEntities): array { - $entities = array(); - - foreach ($rawEntities as $rawEntity) { - array_push($entities, $this->_parseRawEntity($rawEntity, $annotations)); - } - - return $entities; + return array_map( + fn(array $rawEntity) => $this->parseRawEntity($rawEntity), + $rawEntities + ); } /** * Parses raw data to Entity. * - * @param array $rawEntity Raw entity data provided from database. - * @param array $annotations The set of entity annotations. + * @param array $rawEntity Raw entity data provided from database. * - * @return Entity + * @return TEntity * * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException */ - private function _parseRawEntity($rawEntity, $annotations): Entity + private function parseRawEntity(array $rawEntity): IEntity { - /** @var Entity $entity */ - $entity = $this->_class->newInstance($rawEntity); - - if ($annotations[0]->fetchMode === Entity::FETCH_EAGER) { - $properties = $this->_class->getProperties(); - - foreach ($properties as $property) { - if (Annotations::propertyHasAnnotation($entity, $property->name, "@manyToMany")) { - $this->_fetchManyToMany($entity, $property->name); - } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@oneToMany")) { - $this->_fetchOneToMany($entity, $property->name); - } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@manyToOne")) { - $this->_fetchManyToOne($entity, $property->name); - } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@oneToOne")) { - $this->_fetchOneToOne($entity, $property->name); - } + /** @var TEntity $entity */ + $entity = new ($this->entityClass)($rawEntity); + + $properties = new ReflectionClass($this->entityClass)->getProperties(); + + foreach ($properties as $property) { + $attributes = $property->getAttributes(); + + if (array_any($attributes, fn(ReflectionAttribute $attribute) => $attribute->name === ManyToMany::class)) { + $this->handleRelation($entity, $property->name, $this->fetchManyToMany(...)); + } elseif (array_any($attributes, fn(ReflectionAttribute $attribute) => $attribute->name === OneToMany::class)) { + $this->handleRelation($entity, $property->name, $this->fetchOneToMany(...)); + } elseif (array_any($attributes, fn(ReflectionAttribute $attribute) => $attribute->name === ManyToOne::class)) { + $this->handleRelation($entity, $property->name, $this->fetchManyToOne(...)); + } elseif (array_any($attributes, fn(ReflectionAttribute $attribute) => $attribute->name === OneToOne::class)) { + $this->handleRelation($entity, $property->name, $this->fetchOneToOne(...)); } } return $entity; } + + /** + * @throws FacadeException + */ + private function ensureEntityType(IEntity $entity): void + { + if (!$entity instanceof $this->entityClass) { + throw new FacadeException("Cannot perform action on entity. The type of the entity is not valid for this facade."); + } + } + + /** + * Handles a relation property by ensuring to respect the entity fetch mode (EAGER or LAZY). + * + * @param TEntity $entity The managed entity. + * @param string $property The property in relation. + * @param callable $initializer The relation initializer function. + * + * @throws EntityException + */ + private function handleRelation(IEntity $entity, string $property, callable $initializer): void + { + if ($this->entityTableAnnotation->fetchMode === FetchMode::EAGER) { + $entity->{$property} = new Relation($initializer()); + } elseif ($this->entityTableAnnotation->fetchMode === FetchMode::LAZY) { + $entity->{$property} = new ReflectionClass(Relation::class)->newLazyGhost(function (Relation $relation) use ($initializer) { + $relation->__construct($initializer()); + }); + } else { + throw new EntityException("The fetch mode of the entity \"{$this->entityClass}\" is not valid."); + } + } } diff --git a/src/LightQL/Sessions/IFacade.php b/src/LightQL/Sessions/IFacade.php index 9f6f2db..6393155 100644 --- a/src/LightQL/Sessions/IFacade.php +++ b/src/LightQL/Sessions/IFacade.php @@ -1,102 +1,106 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Sessions; -use ElementaryFramework\LightQL\Entities\Entity; +use ElementaryFramework\LightQL\Entities\IEntity; +use ElementaryFramework\LightQL\Entities\IPrimaryKey; +use ElementaryFramework\LightQL\Exceptions\EntityException; +use ElementaryFramework\LightQL\Exceptions\FacadeException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Exceptions\QueryException; /** - * IFacade + * IFacade interface. * * Provide methods for all entity facades. * - * @category Sessions - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Sessions/IFacade + * @template TEntity of IEntity */ interface IFacade { /** * Creates an entity. * - * @param Entity $entity The entity to create. + * @param TEntity $entity The entity to create. + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException */ - function create(Entity &$entity); + function create(IEntity &$entity): void; /** * Edit an entity. * - * @param Entity $entity The entity to edit. + * @param TEntity $entity The entity to edit. + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException */ - function edit(Entity &$entity); + function update(IEntity &$entity): void; /** * Delete an entity. * - * @param Entity $entity The entity to delete. + * @param TEntity $entity The entity to delete. + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException */ - function delete(Entity &$entity); + function delete(IEntity &$entity): void; /** * Find an entity. * - * @param mixed $id The id of the entity to find + * @param int|string|IPrimaryKey $id The id of the entity to find + * @return TEntity * - * @return Entity + * @throws FacadeException + * @throws EntityException + * @throws LightQLException */ - function find($id): Entity; + function retrieve(int|string|IPrimaryKey $id): IEntity; /** * Find all entities. * - * @return Entity[] + * @return list + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException + * @throws QueryException */ - function findAll(): array; + function list(): array; /** * Find all entities in the given range. * - * @param int $start The starting offset. + * @param int $start The starting offset. * @param int $length The number of entities to find. * - * @return Entity[] + * @return list + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException + * @throws QueryException */ - function findRange(int $start, int $length): array; + function range(int $start, int $length): array; /** * Count the number of entities. * * @return int + * + * @throws FacadeException + * @throws EntityException + * @throws LightQLException + * @throws QueryException */ function count(): int; } \ No newline at end of file From 52b795aebd020372a87628c3e7671d8f3384d117 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 00:37:59 +0100 Subject: [PATCH 2/9] wip: Updated `EntityManager` implementation. --- src/LightQL/Attributes/AttributeUtils.php | 121 ++++++++ .../AutoIncrement.php | 2 +- .../{Annotations => Attributes}/Column.php | 2 +- .../{Annotations => Attributes}/Id.php | 2 +- .../IdGenerator.php | 2 +- .../{Annotations => Attributes}/Index.php | 2 +- .../ManyToMany.php | 2 +- .../{Annotations => Attributes}/ManyToOne.php | 2 +- .../NamedQuery.php | 2 +- .../{Annotations => Attributes}/NotNull.php | 2 +- .../{Annotations => Attributes}/OneToMany.php | 2 +- .../{Annotations => Attributes}/OneToOne.php | 2 +- .../PersistenceUnit.php | 2 +- .../{Annotations => Attributes}/Size.php | 2 +- .../{Annotations => Attributes}/Table.php | 2 +- .../{Annotations => Attributes}/Unique.php | 2 +- src/LightQL/Entities/EntityManager.php | 280 ++++++++---------- src/LightQL/Entities/Query.php | 183 ------------ src/LightQL/Entities/Relation.php | 6 +- src/LightQL/LightQL.php | 86 +++++- src/LightQL/Query/Builder.php | 1 - src/LightQL/Query/NamedQuery.php | 157 ++++++++++ src/LightQL/Query/PendingQuery.php | 1 - src/LightQL/Sessions/Facade.php | 32 +- 24 files changed, 501 insertions(+), 396 deletions(-) create mode 100644 src/LightQL/Attributes/AttributeUtils.php rename src/LightQL/{Annotations => Attributes}/AutoIncrement.php (86%) rename src/LightQL/{Annotations => Attributes}/Column.php (95%) rename src/LightQL/{Annotations => Attributes}/Id.php (84%) rename src/LightQL/{Annotations => Attributes}/IdGenerator.php (95%) rename src/LightQL/{Annotations => Attributes}/Index.php (97%) rename src/LightQL/{Annotations => Attributes}/ManyToMany.php (97%) rename src/LightQL/{Annotations => Attributes}/ManyToOne.php (96%) rename src/LightQL/{Annotations => Attributes}/NamedQuery.php (95%) rename src/LightQL/{Annotations => Attributes}/NotNull.php (85%) rename src/LightQL/{Annotations => Attributes}/OneToMany.php (96%) rename src/LightQL/{Annotations => Attributes}/OneToOne.php (97%) rename src/LightQL/{Annotations => Attributes}/PersistenceUnit.php (93%) rename src/LightQL/{Annotations => Attributes}/Size.php (94%) rename src/LightQL/{Annotations => Attributes}/Table.php (94%) rename src/LightQL/{Annotations => Attributes}/Unique.php (97%) delete mode 100644 src/LightQL/Entities/Query.php create mode 100644 src/LightQL/Query/NamedQuery.php diff --git a/src/LightQL/Attributes/AttributeUtils.php b/src/LightQL/Attributes/AttributeUtils.php new file mode 100644 index 0000000..47dfaf1 --- /dev/null +++ b/src/LightQL/Attributes/AttributeUtils.php @@ -0,0 +1,121 @@ +|TClass $class The class on which get the attribute. + * @param class-string $attribute The attribute to get. + * @return TAttribute An instance of the attribute class of found. + * + * @throws ReflectionException When the attribute was not found on the class. + */ + public static function ofClass(string|object $class, string $attribute): object + { + $attributes = new ReflectionClass($class)->getAttributes($attribute); + if (empty($attributes)) { + throw new ReflectionException("The attribute '$class' does not exist on class '$class'."); + } + + return $attributes[0]->newInstance(); + } + + /** + * Gets the attribute instance for a property. + * + * @template TClass of object The class type. + * @template TAttribute of object The attribute type. + * + * @param class-string|TClass $class The class owner of the property. + * @param string $property The name of the property. + * @param class-string $attribute The attribute to get. + * @return TAttribute An instance of the attribute if found. + * + * @throws ReflectionException When the attribute was not found on the property. + */ + public static function ofProperty(string|object $class, string $property, string $attribute): object + { + $attributes = new ReflectionProperty($class, $property)->getAttributes($attribute); + if (empty($attributes)) { + throw new ReflectionException("The attribute '$attribute' does not exist on property '$property' in class '$class'."); + } + + return $attributes[0]->newInstance(); + } + + /** + * Gets the attribute instance for a method. + * + * @template TClass of object The class type. + * @template TAttribute of object The attribute type. + * + * @param class-string|TClass $class The class owner of the method. + * @param string $method The name of the method. + * @param class-string $attribute The attribute to get. + * @return TAttribute An instance of the attribute if found. + * + * @throws ReflectionException When the attribute was not found on the method. + */ + public static function ofMethod(string|object $class, string $method, string $attribute): object + { + $attributes = new ReflectionMethod($class, $method)->getAttributes($attribute); + if (empty($attributes)) { + throw new ReflectionException("The attribute '$attribute' does not exist on method '$method' in class '$class'."); + } + + return $attributes[0]->newInstance(); + } + + /** + * Checks whether a class has the given attribute. + * + * @template TClass of object The class type. + * @template TAttribute of object The attribute type. + * + * @param class-string|TClass $class The class. + * @param class-string $attribute The attribute to check. + * @return bool Whether the class has the given annotation. + * + * @throws ReflectionException + */ + public static function classHasAttribute(string|object $class, string $attribute): bool + { + $attributes = new ReflectionClass($class)->getAttributes($attribute); + return !empty($attributes); + } + + /** + * Checks whether a class property has the given attribute. + * + * @template TClass of object The class type. + * @template TAttribute of object The attribute type. + * + * @param class-string|TClass $class The class owner of the property. + * @param string $property The name of the property. + * @param class-string $attribute The attribute to check. + * @return bool Whether the property has the given annotation. + * + * @throws ReflectionException + */ + public static function propertyHasAttribute(string|object $class, string $property, string $attribute): bool + { + $attributes = new ReflectionProperty($class, $property)->getAttributes($attribute); + return !empty($attributes); + } +} \ No newline at end of file diff --git a/src/LightQL/Annotations/AutoIncrement.php b/src/LightQL/Attributes/AutoIncrement.php similarity index 86% rename from src/LightQL/Annotations/AutoIncrement.php rename to src/LightQL/Attributes/AutoIncrement.php index 114f37e..b96f36a 100644 --- a/src/LightQL/Annotations/AutoIncrement.php +++ b/src/LightQL/Attributes/AutoIncrement.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; diff --git a/src/LightQL/Annotations/Column.php b/src/LightQL/Attributes/Column.php similarity index 95% rename from src/LightQL/Annotations/Column.php rename to src/LightQL/Attributes/Column.php index 59995e3..5177c51 100644 --- a/src/LightQL/Annotations/Column.php +++ b/src/LightQL/Attributes/Column.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/Id.php b/src/LightQL/Attributes/Id.php similarity index 84% rename from src/LightQL/Annotations/Id.php rename to src/LightQL/Attributes/Id.php index 799638a..0ccaddf 100644 --- a/src/LightQL/Annotations/Id.php +++ b/src/LightQL/Attributes/Id.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; diff --git a/src/LightQL/Annotations/IdGenerator.php b/src/LightQL/Attributes/IdGenerator.php similarity index 95% rename from src/LightQL/Annotations/IdGenerator.php rename to src/LightQL/Attributes/IdGenerator.php index f7d6cdf..88c3d84 100644 --- a/src/LightQL/Annotations/IdGenerator.php +++ b/src/LightQL/Attributes/IdGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use ElementaryFramework\LightQL\Entities\IPrimaryKeyGenerator; diff --git a/src/LightQL/Annotations/Index.php b/src/LightQL/Attributes/Index.php similarity index 97% rename from src/LightQL/Annotations/Index.php rename to src/LightQL/Attributes/Index.php index a10ce77..f86f185 100644 --- a/src/LightQL/Annotations/Index.php +++ b/src/LightQL/Attributes/Index.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/ManyToMany.php b/src/LightQL/Attributes/ManyToMany.php similarity index 97% rename from src/LightQL/Annotations/ManyToMany.php rename to src/LightQL/Attributes/ManyToMany.php index 33b12a3..17d615e 100644 --- a/src/LightQL/Annotations/ManyToMany.php +++ b/src/LightQL/Attributes/ManyToMany.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use ElementaryFramework\LightQL\Entities\IEntity; diff --git a/src/LightQL/Annotations/ManyToOne.php b/src/LightQL/Attributes/ManyToOne.php similarity index 96% rename from src/LightQL/Annotations/ManyToOne.php rename to src/LightQL/Attributes/ManyToOne.php index 1fcc8d5..948e8bf 100644 --- a/src/LightQL/Annotations/ManyToOne.php +++ b/src/LightQL/Attributes/ManyToOne.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use ElementaryFramework\LightQL\Entities\IEntity; diff --git a/src/LightQL/Annotations/NamedQuery.php b/src/LightQL/Attributes/NamedQuery.php similarity index 95% rename from src/LightQL/Annotations/NamedQuery.php rename to src/LightQL/Attributes/NamedQuery.php index 111ad9d..405f72a 100644 --- a/src/LightQL/Annotations/NamedQuery.php +++ b/src/LightQL/Attributes/NamedQuery.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/NotNull.php b/src/LightQL/Attributes/NotNull.php similarity index 85% rename from src/LightQL/Annotations/NotNull.php rename to src/LightQL/Attributes/NotNull.php index 0079e20..b04fb64 100644 --- a/src/LightQL/Annotations/NotNull.php +++ b/src/LightQL/Attributes/NotNull.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; diff --git a/src/LightQL/Annotations/OneToMany.php b/src/LightQL/Attributes/OneToMany.php similarity index 96% rename from src/LightQL/Annotations/OneToMany.php rename to src/LightQL/Attributes/OneToMany.php index 4923bc5..a52ae45 100644 --- a/src/LightQL/Annotations/OneToMany.php +++ b/src/LightQL/Attributes/OneToMany.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use ElementaryFramework\LightQL\Entities\IEntity; diff --git a/src/LightQL/Annotations/OneToOne.php b/src/LightQL/Attributes/OneToOne.php similarity index 97% rename from src/LightQL/Annotations/OneToOne.php rename to src/LightQL/Attributes/OneToOne.php index bcdc44c..2c9d672 100644 --- a/src/LightQL/Annotations/OneToOne.php +++ b/src/LightQL/Attributes/OneToOne.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/PersistenceUnit.php b/src/LightQL/Attributes/PersistenceUnit.php similarity index 93% rename from src/LightQL/Annotations/PersistenceUnit.php rename to src/LightQL/Attributes/PersistenceUnit.php index e2b339d..8b61ca8 100644 --- a/src/LightQL/Annotations/PersistenceUnit.php +++ b/src/LightQL/Attributes/PersistenceUnit.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/Size.php b/src/LightQL/Attributes/Size.php similarity index 94% rename from src/LightQL/Annotations/Size.php rename to src/LightQL/Attributes/Size.php index 5e1afc4..5cdab5d 100644 --- a/src/LightQL/Annotations/Size.php +++ b/src/LightQL/Attributes/Size.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/Table.php b/src/LightQL/Attributes/Table.php similarity index 94% rename from src/LightQL/Annotations/Table.php rename to src/LightQL/Attributes/Table.php index 8f30dab..5bee8a6 100644 --- a/src/LightQL/Annotations/Table.php +++ b/src/LightQL/Attributes/Table.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Annotations/Unique.php b/src/LightQL/Attributes/Unique.php similarity index 97% rename from src/LightQL/Annotations/Unique.php rename to src/LightQL/Attributes/Unique.php index 06d7699..8dfd1dd 100644 --- a/src/LightQL/Annotations/Unique.php +++ b/src/LightQL/Attributes/Unique.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ElementaryFramework\LightQL\Annotations; +namespace ElementaryFramework\LightQL\Attributes; use Attribute; use InvalidArgumentException; diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 3244b13..57d2736 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -1,143 +1,103 @@ - * @copyright 2018 Aliens Group - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Entities; -use ElementaryFramework\Annotations\Annotations; -use ElementaryFramework\LightQL\Enums\DBMS; +use ElementaryFramework\LightQL\Attributes\AttributeUtils; +use ElementaryFramework\LightQL\Attributes\Id; +use ElementaryFramework\LightQL\Attributes\Column as ColumnAttribute; +use ElementaryFramework\LightQL\Attributes\IdGenerator; +use ElementaryFramework\LightQL\Attributes\Table; use ElementaryFramework\LightQL\Exceptions\EntityException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Exceptions\QueryException; use ElementaryFramework\LightQL\LightQL; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; +use Exception; +use ReflectionClass; +use ReflectionException; +use Throwable; /** * Entity Manager * - * Manage all entities, using one same persistence unit. - * - * @final - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/EntityManager + * Manage all entities through a single persistence unit. */ -final class EntityManager +final readonly class EntityManager { /** - * The persistence unit of this entity - * manager. - * - * @var PersistenceUnit + * The LightQL instance used by this entity manager. */ - private PersistenceUnit $_persistenceUnit; - - /** - * The LightQL instance used by this - * entity manager. - * - * @var LightQL - */ - private $_lightql; + private LightQL $lightql; /** * EntityManager constructor. * * @param PersistenceUnit $persistenceUnit The persistence unit to use in this manager. * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException */ - public function __construct(PersistenceUnit $persistenceUnit) + public function __construct(private PersistenceUnit $persistenceUnit) { - // Save the persistence unit - $this->_persistenceUnit = $persistenceUnit; - // Create a LightQL instance - $this->_lightql = new LightQL( - database: $this->_persistenceUnit->database, - hostname: $this->_persistenceUnit->hostname, - username: $this->_persistenceUnit->username, - password: $this->_persistenceUnit->password, - dbms: $this->_persistenceUnit->dbms, + $this->lightql = new LightQL( + database: $this->persistenceUnit->database, + hostname: $this->persistenceUnit->hostname, + username: $this->persistenceUnit->username, + password: $this->persistenceUnit->password, + dbms: $this->persistenceUnit->dbms, ); } /** * Finds an entity from the database. * - * @param string $entityClass The class name of the entity to find. - * @param mixed $id The value of the primary key. + * @param class-string $entityClass The class name of the entity to find. + * @param string|int|IPrimaryKey $id The value of the primary key. * * @return array Raw data from database. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * @throws \ReflectionException + * @throws LightQLException + * @throws ReflectionException + * @throws QueryException */ - public function find(string $entityClass, $id): array + public function find(string $entityClass, string|int|IPrimaryKey $id): array { - $entityAnnotation = Annotations::ofClass($entityClass, "@entity"); + $table = AttributeUtils::ofClass($entityClass, Table::class); - /** @var Entity $entity */ + /** @var IEntity $entity */ $entity = new $entityClass; $columns = $entity->getColumns(); - $where = array(); + $where = []; if ($id instanceof IPrimaryKey) { - $pkClass = new \ReflectionClass($id); + $pkClass = new ReflectionClass($id); $properties = $pkClass->getProperties(); - /** @var \ReflectionProperty $property */ foreach ($properties as $property) { - if (Annotations::propertyHasAnnotation($id, $property->getName(), "@id") && Annotations::propertyHasAnnotation($id, $property->getName(), "@column")) { - $name = Annotations::ofProperty($id, $property->getName(), "@column")[0]->name; - $where[$name] = $this->_lightql->quote($id->{$property->getName()}); + if (AttributeUtils::propertyHasAttribute($id, $property->getName(), Id::class) && AttributeUtils::propertyHasAttribute($id, $property->getName(), ColumnAttribute::class)) { + $name = AttributeUtils::ofProperty($id, $property->getName(), ColumnAttribute::class)->name; + $where[$name] = $id->{$property->getName()}; } } } else { - foreach ($columns as $property => $column) { - if (count($where) === 0) { - if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($id); - } - } else break; + foreach ($columns as $column) { + if ($column->isPrimaryKey) { + $where[$column->name] = $id; + break; + } } } - $raw = $this->_lightql - ->from($entityAnnotation[0]->table) + return $this->lightql + ->builder() + ->from($table->table) ->where($where) - ->selectFirst(); - - return $raw; + ->select() + ->execute() + ->fetchFirst(); } /** @@ -145,20 +105,19 @@ public function find(string $entityClass, $id): array * * @param IEntity $entity The entity to create. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException|ReflectionException + * @throws Throwable */ - public function persist(IEntity &$entity) + public function persist(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); $columns = $entity->getColumns(); - $fieldAndValues = array(); + $fieldAndValues = []; $autoIncrementProperty = null; $idProperty = null; - /** @var Column $column */ foreach ($columns as $property => $column) { if ($autoIncrementProperty === null && $column->isAutoIncrement) { $autoIncrementProperty = $property; @@ -167,61 +126,62 @@ public function persist(IEntity &$entity) if ($idProperty === null && $column->isPrimaryKey) { $idProperty = $property; } + + if ($autoIncrementProperty !== null && $idProperty !== null) { + break; + } } if ($idProperty !== null && ($autoIncrementProperty === null || $autoIncrementProperty !== $idProperty)) { // We have a non auto incremented primary key... // Check if the value is null or not set - if ($entity->{$idProperty} === null || !isset($entity->{$idProperty})) { + if (!isset($entity->{$idProperty})) { // We have a not defined non auto incremented primary key... // Check if the entity class has an @idGenerator annotation - if (Annotations::classHasAnnotation($entity, "@idGenerator")) { - $idGeneratorAnnotation = Annotations::ofClass($entity, "@idGenerator"); + if (AttributeUtils::classHasAttribute($entity, IdGenerator::class)) { + $idGeneratorAttribute = AttributeUtils::ofClass($entity, IdGenerator::class); - if (\is_subclass_of($idGeneratorAnnotation[0]->generator, IPrimaryKeyGenerator::class)) { + if (is_subclass_of($idGeneratorAttribute->generator, IPrimaryKeyGenerator::class)) { // We are safe ! // Generate an entity primary key using the generator - $idGeneratorClass = new \ReflectionClass($idGeneratorAnnotation[0]->generator); /** @var IPrimaryKeyGenerator $idGenerator */ - $idGenerator = $idGeneratorClass->newInstance(); + $idGenerator = new ReflectionClass($idGeneratorAttribute->generator)->newInstance(); $entity->{$idProperty} = $idGenerator->generate($entity); } else { // Bad id generator implementation, throw an error - throw new EntityException("The id generator of this entity doesn't implement the IEntityIdGenerator interface."); + throw new EntityException("The id generator of this entity doesn't implement the IPrimaryKeyGenerator interface."); } } else { // This will result to a SQL error, throw instead throw new EntityException( - "Cannot persist an entity into the database. The entity primary key has no value, and has not the @autoIncrement annotation." . - " If the table primary key column is auto incremented, consider add the @autoIncrement annotation to the primary key class property." . - " If the table primary key column is not auto incremented, please give a value to the primary key class property before persist the entity, or use a @idGenerator annotation instead." + "Cannot persist an entity into the database. The entity primary key has no value, and has not the AutoIncrement attribute." . + " If the table primary key column is auto incremented, consider adding the AutoIncrement attribute to the primary key class property." . + " If the table primary key column is not auto incremented, please give a value to the primary key class property before to persist the entity, or use a IdGenerator attribute instead." ); } } } - foreach ($columns as $property => $column) { - $fieldAndValues[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + foreach ($columns as $column) { + $fieldAndValues[$column->name] = $entity->get($column->name); } - $this->_lightql->beginTransaction(); - try { - $this->_lightql - ->from($entityAnnotation[0]->table) - ->insert($fieldAndValues); + $this->lightql->transaction(function () use ($entity, $fieldAndValues, $table, $autoIncrementProperty) { + $statement = $this->lightql + ->builder() + ->from($table->table) + ->insert($fieldAndValues) + ->execute(); if ($autoIncrementProperty !== null) { - $entity->$autoIncrementProperty = $this->_lightql->lastInsertID(); + $entity->$autoIncrementProperty = $statement->getLastInsertId(); } - $this->_lightql->commit(); - } catch (\Exception $e) { - $this->_lightql->rollback(); - + }, function (Throwable $e) { throw new EntityException($e->getMessage()); - } + }); } /** @@ -229,56 +189,53 @@ public function persist(IEntity &$entity) * * @param IEntity $entity The entity to edit. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException + * @throws ReflectionException */ - public function merge(IEntity &$entity) + public function merge(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); $columns = $entity->getColumns(); - $fieldAndValues = array(); + $fieldAndValues = []; - $where = array(); + $where = []; - $entityReflection = new \ReflectionClass($entity); + $entityReflection = new ReflectionClass($entity); $entityProperties = $entityReflection->getProperties(); - /** @var \ReflectionProperty $property */ foreach ($entityProperties as $property) { $id = $entity->{$property->getName()}; + if ($id instanceof IPrimaryKey) { - $propertyReflection = new \ReflectionClass($id); + $propertyReflection = new ReflectionClass($id); $propertyProperties = $propertyReflection->getProperties(); foreach ($propertyProperties as $key) { - $name = Annotations::ofProperty($id, $key->getName(), "@column")[0]->name; - $where[$name] = $this->_lightql->quote($id->{$key->getName()}); + $name = AttributeUtils::ofProperty($id, $key->getName(), ColumnAttribute::class)->name; + $where[$name] = $id->{$key->getName()}; } break; } } - foreach ($columns as $property => $column) { - $fieldAndValues[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + foreach ($columns as $column) { + $fieldAndValues[$column->name] = $entity->get($column->name); if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->name] = $entity->get($column->name); } } - $this->_lightql->beginTransaction(); try { - $this->_lightql - ->from($entityAnnotation[0]->table) + $this->lightql + ->builder() + ->from($table->table) ->where($where) - ->update($fieldAndValues); - - $this->_lightql->commit(); - } catch (\Exception $e) { - $this->_lightql->rollback(); - + ->update($fieldAndValues) + ->execute(); + } catch (Exception $e) { throw new EntityException($e->getMessage()); } } @@ -288,32 +245,30 @@ public function merge(IEntity &$entity) * * @param IEntity $entity The entity to delete. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException + * @throws ReflectionException */ - public function delete(IEntity &$entity) + public function delete(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); $columns = $entity->getColumns(); - $fieldAndValues = array(); - $where = array(); - $pk = array(); + $where = []; + $pk = []; - $entityReflection = new \ReflectionClass($entity); + $entityReflection = new ReflectionClass($entity); $entityProperties = $entityReflection->getProperties(); - /** @var \ReflectionProperty $property */ foreach ($entityProperties as $property) { $id = $entity->{$property->getName()}; if ($id instanceof IPrimaryKey) { - $propertyReflection = new \ReflectionClass($id); + $propertyReflection = new ReflectionClass($id); $propertyProperties = $propertyReflection->getProperties(); foreach ($propertyProperties as $key) { - $name = Annotations::ofProperty($id, $key->getName(), "@column")[0]->name; - $where[$name] = $this->_lightql->quote($id->{$key->getName()}); + $name = AttributeUtils::ofProperty($id, $key->getName(), ColumnAttribute::class)->name; + $where[$name] = $id->{$key->getName()}; $pk[] = $property->getName(); } @@ -323,28 +278,25 @@ public function delete(IEntity &$entity) foreach ($columns as $property => $column) { if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->name] = $entity->get($column->name); $pk[] = $property; } } - $this->_lightql->beginTransaction(); try { - $this->_lightql - ->from($entityAnnotation[0]->table) + $this->lightql + ->builder() + ->from($table->table) ->where($where) - ->delete(); + ->delete() + ->execute(); - if (count($pk) > 0) { + if (!empty($pk)) { foreach ($pk as $item) { $entity->$item = null; } } - - $this->_lightql->commit(); - } catch (\Exception $e) { - $this->_lightql->rollback(); - + } catch (Exception $e) { throw new EntityException($e->getMessage()); } } @@ -357,6 +309,6 @@ public function delete(IEntity &$entity) */ public function getLightQL(): LightQL { - return $this->_lightql; + return $this->lightql; } } diff --git a/src/LightQL/Entities/Query.php b/src/LightQL/Entities/Query.php deleted file mode 100644 index 528fdd4..0000000 --- a/src/LightQL/Entities/Query.php +++ /dev/null @@ -1,183 +0,0 @@ - - * @copyright 2018 Aliens Group - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Entities; - -use ElementaryFramework\LightQL\Exceptions\QueryException; -use ReflectionClass; -use ReflectionException; - -/** - * Query - * - * Manage, run and get results from named queries. - * - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/Query - */ -class Query -{ - /** - * The entity manager running this query. - * - * @var EntityManager - */ - private $_entityManager; - - /** - * The reflection class of the managed entity. - * - * @var ReflectionClass - */ - private $_entityReflection; - - /** - * The named query string. - * - * @var string - */ - private $_namedQuery; - - /** - * Query parameters. - * - * @var array - */ - private $_parameters = array(); - - /** - * The query executed by this instance. - * - * @var \PDOStatement - */ - private $_query = null; - - /** - * Query constructor. - * - * @param EntityManager $manager - */ - public function __construct(EntityManager $manager) - { - $this->_entityManager = $manager; - } - - /** - * Sets the reflection class of the managed entity. - * - * @param class-string|IEntity $entity The managed entity reflection class instance. - * @throws ReflectionException - */ - public function setEntity(string|IEntity $entity): self - { - $this->_entityReflection = new ReflectionClass($entity); - - return $this; - } - - /** - * Sets the named query to execute. - * - * @param string $query The named query. - */ - public function setQuery(string $query): self - { - $this->_namedQuery = $query; - - return $this; - } - - /** - * Defines the value of one of query parameters. - * - * @param string $name The name of the parameter in the query. - * @param mixed $value The value of this parameter. - */ - public function setParam(string $name, $value): self - { - $this->_parameters[$name] = $value; - - return $this; - } - - /** - * Executes the query. - * - * @return bool - */ - public function run(): bool - { - try { - $this->_query = $this->_entityManager->getLightQL()->prepare($this->_namedQuery); - - foreach ($this->_parameters as $name => $value) { - $this->_query->bindValue($name, $value); - } - - return $this->_query->execute(); - } catch (\Exception $e) { - throw new QueryException($e->getMessage()); - } - } - - /** - * Returns the set of results after the execution of the query. - * - * @return Entity[] - */ - public function getResults(): array - { - if ($this->_query === null) { - throw new QueryException("Cannot get results, have you ran the query?"); - } - - $results = array_map(function ($item) { - return $this->_entityReflection->newInstance($item); - }, $this->_query->fetchAll()); - - return $results; - } - - /** - * Returns the first result of the set after the execution - * of the query. - * - * @return IEntity|null - */ - public function getFirstResult(): ?IEntity - { - $results = $this->getResults(); - return count($results) > 0 ? $results[0] : null; - } -} diff --git a/src/LightQL/Entities/Relation.php b/src/LightQL/Entities/Relation.php index 7e11fa3..ac6fa1d 100644 --- a/src/LightQL/Entities/Relation.php +++ b/src/LightQL/Entities/Relation.php @@ -7,13 +7,15 @@ /** * Represents a relationship or association between entities. * - * @template TEntity of IEntity + * @template TEntity of IEntity The type of the related entity. * @template TRelated of TEntity|list = TEntity + * + * @property-read TRelated $related The related data. Can be a reference to a single entity or a collection of entities of the same type. */ class Relation { public function __construct( - public private(set) IEntity|array $related + private(set) IEntity|array $related ) { } diff --git a/src/LightQL/LightQL.php b/src/LightQL/LightQL.php index a5fcf4e..c091202 100644 --- a/src/LightQL/LightQL.php +++ b/src/LightQL/LightQL.php @@ -6,10 +6,12 @@ use ElementaryFramework\LightQL\Enums\DBMS; use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Persistence\PersistenceUnit; use ElementaryFramework\LightQL\Query\Builder; use PDO; use PDOException; use PDOStatement; +use Throwable; /** * Query builder class with parameter binding and type safety. @@ -19,7 +21,7 @@ final class LightQL /** * The current PDO instance. */ - public private(set) ?PDO $pdo = null; + private(set) ?PDO $pdo = null; /** * Class constructor. @@ -37,17 +39,18 @@ final class LightQL * @throws LightQLException */ public function __construct( - private readonly string $database, - private readonly string $hostname = '', - private readonly string $username = '', - private readonly string $password = '', - public private(set) readonly DBMS $dbms = DBMS::SQLITE, - private readonly array $pdoOptions = [], - private readonly ?int $port = null, - private readonly ?string $socket = null, - private readonly ?string $charset = null, - ?string $customDsn = null, - ) { + private readonly string $database, + private readonly string $hostname = '', + private readonly string $username = '', + private readonly string $password = '', + private(set) readonly DBMS $dbms = DBMS::SQLITE, + private readonly array $pdoOptions = [], + private readonly ?int $port = null, + private readonly ?string $socket = null, + private readonly ?string $charset = null, + ?string $customDsn = null, + ) + { $dsn = $customDsn ?? $this->buildDsn(); $commands = $this->getInitCommands(); @@ -58,6 +61,7 @@ public function __construct( * Factory method for backward compatibility with array options. * * @param array $options + * * @throws LightQLException */ public static function fromArray(array $options): self @@ -98,6 +102,24 @@ public static function fromArray(array $options): self ); } + /** + * Factory method used to create a new LightQL instance using a persistence unit. + * + * @param PersistenceUnit $persistenceUnit The persistence unit. + * + * @throws LightQLException + */ + public static function fromPersistenceUnit(PersistenceUnit $persistenceUnit): self + { + return new self( + database: $persistenceUnit->database, + hostname: $persistenceUnit->hostname, + username: $persistenceUnit->username, + password: $persistenceUnit->password, + dbms: $persistenceUnit->dbms, + ); + } + /** * Close the database connection. */ @@ -167,6 +189,44 @@ public function quote(string $value): string return $this->pdo->quote($value); } + /** + * Execute the given callback in a transaction, and automatically commit on success. + * + * @param callable(): void $callback The callback to execute in a transaction. + * @param null|callable(Throwable): bool $onError An optional error callback with the exception occurred while executing the callback. This function can + * return a boolean saying whether the transaction should be commited or not. + * + * @throws Throwable + */ + public function transaction(callable $callback, ?callable $onError = null): void + { + $this->beginTransaction(); + + try { + $callback(); + $this->commit(); + } catch (Throwable $e) { + $shouldCommit = false; + $lastException = $e; + + try { + if ($onError !== null) { + $shouldCommit = $onError($e); + } + } catch (Throwable $e) { + $lastException = $e; + } + + if ($shouldCommit) { + $this->commit(); + } else { + $this->rollBack(); + } + + throw $lastException; + } + } + /** * Begin transaction. */ @@ -328,8 +388,6 @@ private function buildMsSqlDsnAttributes(string &$driver): array */ private function getInitCommands(): array { - $commands = []; - $commands = match ($this->dbms) { DBMS::MYSQL, DBMS::MARIADB => ['SET SQL_MODE=ANSI_QUOTES'], DBMS::MSSQL => ['SET QUOTED_IDENTIFIER ON', 'SET ANSI_NULLS ON'], diff --git a/src/LightQL/Query/Builder.php b/src/LightQL/Query/Builder.php index a36b834..6d98e91 100644 --- a/src/LightQL/Query/Builder.php +++ b/src/LightQL/Query/Builder.php @@ -10,7 +10,6 @@ use ElementaryFramework\LightQL\Exceptions\LightQLException; use ElementaryFramework\LightQL\Exceptions\QueryException; use ElementaryFramework\LightQL\LightQL; -use PDO; /** * Query builder class for constructing SQL queries. diff --git a/src/LightQL/Query/NamedQuery.php b/src/LightQL/Query/NamedQuery.php new file mode 100644 index 0000000..3e61373 --- /dev/null +++ b/src/LightQL/Query/NamedQuery.php @@ -0,0 +1,157 @@ + + */ + private array $_parameters = []; + + /** + * The query executed by this instance. + * + * @var PDOStatement|null + */ + private ?PDOStatement $query = null; + + /** + * Query constructor. + * + * @param EntityManager $manager The entity manager instance. + */ + public function __construct(EntityManager $manager) + { + $this->entityManager = $manager; + } + + /** + * Sets the reflection class of the managed entity. + * + * @param class-string|IEntity $entity The managed entity reflection class instance. + * @throws ReflectionException + */ + public function setEntity(string|IEntity $entity): self + { + $this->entityReflection = new ReflectionClass($entity); + + return $this; + } + + /** + * Sets the named query to execute. + * + * @param string $query The named query. + */ + public function setQuery(string $query): self + { + $this->namedQuery = $query; + + return $this; + } + + /** + * Defines the value of one of query parameters. + * + * @param string|array $name The name of the parameter in the query, or the array of parameters. + * @param mixed $value The value of this parameter. + */ + public function setParam(string|array $name, mixed $value = null): self + { + if (is_string($name)) { + $this->_parameters[$name] = $value; + } else { + $this->_parameters = $name; + } + + return $this; + } + + /** + * Executes the query. + * + * @return bool + * @throws QueryException + */ + public function run(): bool + { + try { + $this->query = $this->entityManager->getLightQL()->prepare($this->namedQuery); + + foreach ($this->_parameters as $name => $value) { + $this->query->bindValue($name, $value); + } + + return $this->query->execute(); + } catch (Exception $e) { + throw new QueryException($e->getMessage()); + } + } + + /** + * Returns the set of results after the execution of the query. + * + * @return IEntity[] + * + * @throws QueryException + * @throws ReflectionException + */ + public function getResults(): array + { + if ($this->query === null) { + throw new QueryException("Cannot get results, have you ran the query?"); + } + + return array_map(function ($item) { + return $this->entityReflection->newInstance($item); + }, $this->query->fetchAll()); + } + + /** + * Returns the first result of the set after the execution + * of the query. + * + * @return IEntity|null + * + * @throws QueryException + * @throws ReflectionException + */ + public function getFirstResult(): ?IEntity + { + $results = $this->getResults(); + return count($results) > 0 ? $results[0] : null; + } +} diff --git a/src/LightQL/Query/PendingQuery.php b/src/LightQL/Query/PendingQuery.php index f77c20f..724e102 100644 --- a/src/LightQL/Query/PendingQuery.php +++ b/src/LightQL/Query/PendingQuery.php @@ -9,7 +9,6 @@ use ElementaryFramework\LightQL\LightQL; use PDO; use PDOException; -use PDOStatement; use Stringable; /** diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index 7777f8d..0dae571 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -4,18 +4,17 @@ namespace ElementaryFramework\LightQL\Sessions; -use ElementaryFramework\LightQL\Annotations\Column; -use ElementaryFramework\LightQL\Annotations\ManyToMany; -use ElementaryFramework\LightQL\Annotations\ManyToOne; -use ElementaryFramework\LightQL\Annotations\NamedQuery; -use ElementaryFramework\LightQL\Annotations\OneToMany; -use ElementaryFramework\LightQL\Annotations\OneToOne; -use ElementaryFramework\LightQL\Annotations\PersistenceUnit as PersistenceUnitAnnotation; -use ElementaryFramework\LightQL\Annotations\Table; +use ElementaryFramework\LightQL\Attributes\Column; +use ElementaryFramework\LightQL\Attributes\ManyToMany; +use ElementaryFramework\LightQL\Attributes\ManyToOne; +use ElementaryFramework\LightQL\Attributes\NamedQuery as NamedQueryAttribute; +use ElementaryFramework\LightQL\Attributes\OneToMany; +use ElementaryFramework\LightQL\Attributes\OneToOne; +use ElementaryFramework\LightQL\Attributes\PersistenceUnit as PersistenceUnitAttribute; +use ElementaryFramework\LightQL\Attributes\Table; use ElementaryFramework\LightQL\Entities\EntityManager; use ElementaryFramework\LightQL\Entities\IEntity; use ElementaryFramework\LightQL\Entities\IPrimaryKey; -use ElementaryFramework\LightQL\Entities\Query; use ElementaryFramework\LightQL\Entities\Relation; use ElementaryFramework\LightQL\Enums\FetchMode; use ElementaryFramework\LightQL\Exceptions\EntityException; @@ -24,6 +23,7 @@ use ElementaryFramework\LightQL\Exceptions\PersistenceUnitException; use ElementaryFramework\LightQL\Exceptions\QueryException; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; +use ElementaryFramework\LightQL\Query\NamedQuery; use ReflectionAttribute; use ReflectionClass; use ReflectionException; @@ -68,7 +68,7 @@ abstract class Facade implements IFacade public function __construct(string|IEntity $class) { $propAttributes = new ReflectionProperty($this, 'entityManager') - ->getAttributes(PersistenceUnitAnnotation::class); + ->getAttributes(PersistenceUnitAttribute::class); if (empty($propAttributes)) { throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no PersistenceUnit attribute."); @@ -91,7 +91,7 @@ public function __construct(string|IEntity $class) $entityTableAnnotation = $classAttributes[0]->newInstance(); $this->entityTableAnnotation = $entityTableAnnotation; - /** @var PersistenceUnitAnnotation $persistenceUnit */ + /** @var PersistenceUnitAttribute $persistenceUnit */ $persistenceUnit = $propAttributes[0]->newInstance(); $this->entityManager = new EntityManager(PersistenceUnit::create($persistenceUnit->name)); } @@ -206,15 +206,15 @@ public function count(): int * * @param string $name The name of the query. * - * @return Query + * @return NamedQuery * * @throws FacadeException * @throws ReflectionException */ - public function getNamedQuery(string $name): Query + public function getNamedQuery(string $name): NamedQuery { $reflection = new ReflectionClass($this->entityClass); - $attributes = array_map(fn(ReflectionAttribute $attribute) => $attribute->newInstance(), $reflection->getAttributes(NamedQuery::class)); + $attributes = array_map(fn(ReflectionAttribute $attribute) => $attribute->newInstance(), $reflection->getAttributes(NamedQueryAttribute::class)); if (empty($attributes)) { throw new FacadeException("The {$reflection->name} class has no NamedQuery attribute."); @@ -222,7 +222,7 @@ public function getNamedQuery(string $name): Query $query = null; - /** @var NamedQuery $namedQuery */ + /** @var NamedQueryAttribute $namedQuery */ foreach ($attributes as $namedQuery) { if ($namedQuery->name === $name) { $query = $namedQuery->query; @@ -234,7 +234,7 @@ public function getNamedQuery(string $name): Query throw new FacadeException("The {$reflection->name} class has no NamedQuery attribute with the name {$name}."); } - return new Query($this->entityManager) + return new NamedQuery($this->entityManager) ->setEntity($this->entityClass) ->setQuery($query); } From ae5c4a2fde2a0eef20c1da1dc271ad9c43db3d28 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 01:05:15 +0100 Subject: [PATCH 3/9] wip: Updated `Entity` implementation. --- src/LightQL/Entities/Column.php | 75 +++++++++- src/LightQL/Entities/Entity.php | 237 +++++++++---------------------- src/LightQL/Entities/IEntity.php | 2 +- 3 files changed, 138 insertions(+), 176 deletions(-) diff --git a/src/LightQL/Entities/Column.php b/src/LightQL/Entities/Column.php index a836823..0e34274 100644 --- a/src/LightQL/Entities/Column.php +++ b/src/LightQL/Entities/Column.php @@ -4,6 +4,18 @@ namespace ElementaryFramework\LightQL\Entities; +use ElementaryFramework\LightQL\Attributes\AttributeUtils; +use ElementaryFramework\LightQL\Attributes\AutoIncrement; +use ElementaryFramework\LightQL\Attributes\Column as ColumnAttribute; +use ElementaryFramework\LightQL\Attributes\Id; +use ElementaryFramework\LightQL\Attributes\ManyToMany; +use ElementaryFramework\LightQL\Attributes\ManyToOne; +use ElementaryFramework\LightQL\Attributes\OneToMany; +use ElementaryFramework\LightQL\Attributes\OneToOne; +use ElementaryFramework\LightQL\Attributes\Size; +use ElementaryFramework\LightQL\Attributes\Unique; +use ReflectionException; + /** * Column * @@ -40,37 +52,44 @@ class Column /** * Defines if the column has the AUTO_INCREMENT constraint. */ - public bool $isAutoIncrement; + private(set) bool $isAutoIncrement; /** * Defines if the column is a primary key. */ - public bool $isPrimaryKey; + private(set) bool $isPrimaryKey; /** * Defines if the column has the UNIQUE constraint. */ - public bool $isUniqueKey; + private(set) bool $isUniqueKey; /** * Defines if the column is in a one-to-many relation with another. */ - public bool $isOneToMany; + private(set) bool $isOneToMany; /** * Defines if the column is in a many-to-one relation with another. */ - public bool $isManyToOne; + private(set) bool $isManyToOne; /** * Defines if the column is in a many-to-many relation with another. */ - public bool $isManyToMany; + private(set) bool $isManyToMany; /** * Defines if the column is in a one-to-one relation with another. */ - public bool $isOneToOne; + private(set) bool $isOneToOne; + + /** + * Checks whether the column is a relation. + */ + public bool $isRelation { + get => $this->isManyToMany || $this->isOneToMany || $this->isManyToOne || $this->isOneToOne; + } /** * Create a new instance of the table column descriptor. @@ -87,4 +106,46 @@ public function __construct(string $name, string $type, array $size, mixed $defa $this->size = $size; $this->default = $default; } + + /** + * Create a new column from the metadata of the given entity property. + * + * @template TClass of object The entity class. + * + * @param class-string|TClass $entity The entity class name or instance. + * @param string $property The name of the property. + * + * @throws ReflectionException + * + * @internal + */ + public static function ofEntityProperty(string|object $entity, string $property): self + { + $columnAttribute = AttributeUtils::ofProperty($entity, $property, COlumnAttribute::class); + + try { + $sizeAttribute = AttributeUtils::ofProperty($entity, $property, Size::class); + } catch (ReflectionException) { + $sizeAttribute = null; + } + + $name = $columnAttribute->name; + $type = $columnAttribute->type ?? ''; + $size = [ + $sizeAttribute?->min, + $sizeAttribute?->max, + ]; + + $column = new self($name, $type, $size, $columnAttribute->default); + + $column->isPrimaryKey = AttributeUtils::propertyHasAttribute($entity, $property, Id::class); + $column->isUniqueKey = $column->isPrimaryKey || AttributeUtils::propertyHasAttribute($entity, $property, Unique::class); + $column->isAutoIncrement = AttributeUtils::propertyHasAttribute($entity, $property, AutoIncrement::class); + $column->isManyToMany = AttributeUtils::propertyHasAttribute($entity, $property, ManyToMany::class); + $column->isManyToOne = AttributeUtils::propertyHasAttribute($entity, $property, ManyToOne::class); + $column->isOneToMany = AttributeUtils::propertyHasAttribute($entity, $property, OneToMany::class); + $column->isOneToOne = AttributeUtils::propertyHasAttribute($entity, $property, OneToOne::class); + + return $column; + } } diff --git a/src/LightQL/Entities/Entity.php b/src/LightQL/Entities/Entity.php index bc2ae51..2290f61 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -1,126 +1,68 @@ - * @copyright 2018 Aliens Group - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); namespace ElementaryFramework\LightQL\Entities; -use ElementaryFramework\Annotations\Annotations; -use ElementaryFramework\Annotations\IAnnotation; +use ElementaryFramework\LightQL\Attributes\AttributeUtils; +use ElementaryFramework\LightQL\Attributes\Table; +use ElementaryFramework\LightQL\Attributes\Column as ColumnAttribute; use ElementaryFramework\LightQL\Exceptions\EntityException; -use ElementaryFramework\LightQL\Exceptions\AnnotationException; +use ReflectionClass; +use ReflectionException; /** * Entity * - * Object Oriented Mapping of a database row. - * - * @abstract - * @category Entities - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/Entity + * Object-Oriented Mapping of a database row. */ abstract class Entity implements IEntity { - /** - * Fetch data in eager mode. - */ - const FETCH_EAGER = 1; - - /** - * Fetch data in lazy mode. - */ - const FETCH_LAZY = 2; - /** * The raw data provided from the database. * - * @var array + * @var array */ - protected $raw = array(); + protected array $raw = []; /** * The reflection class of this entity. * - * @var \ReflectionClass + * @var ReflectionClass|null */ - private $_reflection = null; + private ?ReflectionClass $reflection = null; /** * The array of database columns of this entity. * * @var Column[] */ - private $_columns = array(); + private array $columns = []; /** * Entity constructor. * - * @param array $data The raw database data. + * @param array $data The raw table row data. * * @throws EntityException - * @throws AnnotationException - * @throws \ReflectionException + * @throws ReflectionException */ - public function __construct(array $data = array()) + public function __construct(array $data = []) { - if (!Annotations::classHasAnnotation($this, "@entity")) { - throw new EntityException("Cannot create an entity without the @entity annotation."); + if (!AttributeUtils::classHasAttribute($this, Table::class)) { + throw new EntityException("Cannot create an entity without the Table attribute."); } - $this->_reflection = new \ReflectionClass($this); - $properties = $this->_reflection->getProperties(); + $this->reflection = new ReflectionClass($this); + $properties = $this->reflection->getProperties(); $pkFound = false; foreach ($properties as $property) { - if ($this->_propertyHasAnnotation($property->name, "@column")) { - $name = $this->_getMetadata($property->name, "@column", "name"); - $type = $this->_getMetadata($property->name, "@column", "type", ""); - $size = array( - $this->_getMetadata($property->name, '@size', "min"), - $this->_getMetadata($property->name, '@size', "max") - ); - - $column = new Column($name, $type, $size); - - $column->isPrimaryKey = $this->_propertyHasAnnotation($property->name, '@id'); - $column->isUniqueKey = $column->isPrimaryKey || $this->_propertyHasAnnotation($property->name, '@unique'); - $column->isAutoIncrement = $this->_propertyHasAnnotation($property->name, '@autoIncrement'); - $column->isManyToMany = $this->_propertyHasAnnotation($property->name, '@manyToMany'); - $column->isManyToOne = $this->_propertyHasAnnotation($property->name, '@manyToOne'); - $column->isOneToMany = $this->_propertyHasAnnotation($property->name, '@oneToMany'); - $column->isOneToOne = $this->_propertyHasAnnotation($property->name, '@oneToOne'); + if ($this->propertyHasAttribute($property->name, ColumnAttribute::class)) { + $column = Column::ofEntityProperty($this, $property->name); - $this->_columns[$property->name] = $column; + $this->columns[$property->name] = $column; if ($column->isPrimaryKey && $pkFound) { throw new EntityException("The entity has declared more than one primary keys. Consider using a class implementing the IPrimaryKey interface instead."); @@ -134,11 +76,9 @@ public function __construct(array $data = array()) } /** - * Populates data in the entity. - * - * @param array $data The raw database data. + * {@inheritDoc} */ - public function hydrate(array $data) + public function hydrate(array $data): void { // Merge values foreach ($data as $name => $value) { @@ -146,13 +86,12 @@ public function hydrate(array $data) } // Populate @column properties - foreach ($this->_columns as $property => $column) { - if (!$column->isManyToMany && !$column->isManyToOne - && !$column->isOneToMany && !$column->isOneToOne) { - if (array_key_exists($column->getName(), $this->raw)) { - $this->{$property} = $this->raw[$column->getName()]; - } elseif (\is_null($this->{$property}) || $this->{$property} === null) { - $this->{$property} = $column->getDefault(); + foreach ($this->columns as $property => $column) { + if (!$column->isRelation) { + if (array_key_exists($column->name, $this->raw)) { + $this->{$property} = $this->raw[$column->name]; + } elseif ($this->{$property} === null) { + $this->{$property} = $column->default; } } else { $this->{$property} = null; @@ -161,122 +100,84 @@ public function hydrate(array $data) } /** - * Gets the raw value of a table column. - * - * @param string $column The table column name. - * - * @return mixed + * {@inheritDoc} */ - public function get(string $column) + public function get(string $column): mixed { // Try to get the raw value - if ($this->_exists($column)) { + if ($this->exists($column)) { return $this->raw[$column]; } // Try to get the property value - /** @var Column $c */ - foreach ($this->_columns as $property => $c) { - if ($c->getName() === $column && isset($this->{$property})) { - if ($this->{$property} instanceof Entity) { - // Have to be a reference, not a collection - if ($c->isManyToOne || $c->isManyToMany) { - // Find the good property - continue; - } else if ($c->isOneToMany) { - // Resolve the referenced column - $referencedColumn = $this->_getMetadata($property, "@oneToMany", "referencedColumn"); - return $this->{$property}->get($referencedColumn); - } else if ($c->isOneToOne) { - // Resolve the referenced column - $referencedColumn = $this->_getMetadata($property, "@oneToOne", "referencedColumn"); - return $this->{$property}->get($referencedColumn); - } - } else { - return $this->{$property}; - } - } - } + // TODO +// foreach ($this->columns as $property => $c) { +// if ($c->name === $column && isset($this->{$property})) { +// if ($this->{$property} instanceof Entity) { +// // Have to be a reference, not a collection +// if ($c->isManyToOne || $c->isManyToMany) { +// // Find the good property +// continue; +// } else if ($c->isOneToMany) { +// // Resolve the referenced column +// $referencedColumn = $this->_getMetadata($property, "@oneToMany", "referencedColumn"); +// return $this->{$property}->get($referencedColumn); +// } else if ($c->isOneToOne) { +// // Resolve the referenced column +// $referencedColumn = $this->_getMetadata($property, "@oneToOne", "referencedColumn"); +// return $this->{$property}->get($referencedColumn); +// } +// } else { +// return $this->{$property}; +// } +// } +// } // The value definitively doesn't exist return null; } /** - * Sets the raw value of a table column. - * - * @param string $column The table column name. - * @param mixed $value The table column value. + * {@inheritDoc} */ - public function set(string $column, $value) + public function set(string $column, $value): void { - $this->hydrate(array($column => $value)); + $this->hydrate([$column => $value]); } /** - * Gets the list of table columns - * of this entity. - * - * @return Column[] + * {@inheritDoc} */ public function getColumns(): array { - return $this->_columns; + return $this->columns; } /** - * Checks if a property has the given annotation. - * - * @param string $property The name of the property. - * @param string $annotation The name of the annotation. + * Checks if a property has the given attribute. * - * @return bool - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - */ - private function _propertyHasAnnotation(string $property, string $annotation): bool - { - return Annotations::propertyHasAnnotation($this, $property, $annotation); - } - - /** - * Returns the annotation, or the value of an annotation property - * of a property. + * @template TAttribute Attribute class. * * @param string $property The name of the property. - * @param string $type The name of the annotation. - * @param string $name The name of the annotation property to retrieve. - * Set it to null to retrieve the entire annotation object. - * @param mixed $default The default value to return if the property has no - * annotation of the given type. + * @param class-string $annotation The name of the attribute. * - * @return IAnnotation|mixed + * @return bool * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException + * @throws ReflectionException */ - private function _getMetadata(string $property, string $type, ?string $name = null, $default = null) + private function propertyHasAttribute(string $property, string $annotation): bool { - $a = Annotations::ofProperty($this, $property, $type); - - if (!count($a)) { - return $default; - } - - if ($name === null) { - return $a[0]; - } - - return $a[0]->$name !== null ? $a[0]->$name : $default; + return AttributeUtils::propertyHasAttribute($this, $property, $annotation); } /** - * Checks if the given column name exists in this entity. + * Checks if the given column name exists in the raw values of this entity. * * @param string $column The column name to search. * * @return bool */ - private function _exists(string $column): bool + private function exists(string $column): bool { return array_key_exists($column, $this->raw); } diff --git a/src/LightQL/Entities/IEntity.php b/src/LightQL/Entities/IEntity.php index de1758b..d51d27c 100644 --- a/src/LightQL/Entities/IEntity.php +++ b/src/LightQL/Entities/IEntity.php @@ -15,7 +15,7 @@ interface IEntity /** * Populates data in the entity. * - * @param array $data The raw database data. + * @param array $data The raw database data. */ public function hydrate(array $data): void; From 8215153eef5f86918cbc4d690fc28484049eeaa2 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 01:58:40 +0100 Subject: [PATCH 4/9] wip: Added support for value validation. --- src/LightQL/Attributes/IdGenerator.php | 8 ++-- src/LightQL/Attributes/Validator.php | 43 ++++++++++++++++++ src/LightQL/Entities/Column.php | 45 +++++++++++++++++-- src/LightQL/Entities/EntityManager.php | 33 +++++++++----- src/LightQL/Entities/IValidator.php | 29 ++++++++++++ .../Exceptions/ValidationException.php | 31 +++++++++++++ 6 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 src/LightQL/Attributes/Validator.php create mode 100644 src/LightQL/Entities/IValidator.php create mode 100644 src/LightQL/Exceptions/ValidationException.php diff --git a/src/LightQL/Attributes/IdGenerator.php b/src/LightQL/Attributes/IdGenerator.php index 88c3d84..a943a43 100644 --- a/src/LightQL/Attributes/IdGenerator.php +++ b/src/LightQL/Attributes/IdGenerator.php @@ -15,7 +15,9 @@ * * This attribute should be used on a class with the {@see Table} attribute to have effect. * - * @property-read class-string $generator Specify the class name to use as the ID generator of the current entity. + * @template TGenerator of IPrimaryKeyGenerator The generator class. + * + * @property-read class-string $generator Specify the class name to use as the ID generator of the current entity. */ #[Attribute(Attribute::TARGET_CLASS)] class IdGenerator @@ -23,7 +25,7 @@ class IdGenerator /** * Initialize the id generator attribute. * - * @param class-string $generator The ID generator class name + * @param class-string $generator The ID generator class name. * * @throws InvalidArgumentException */ @@ -31,7 +33,7 @@ public function __construct( public readonly string $generator ) { if (empty($generator)) { - throw new InvalidArgumentException(self::class . " must have a non-empty \"generator\" property"); + throw new InvalidArgumentException(self::class . ' must have a non-empty "generator" property'); } if (!is_subclass_of($generator, IPrimaryKeyGenerator::class)) { diff --git a/src/LightQL/Attributes/Validator.php b/src/LightQL/Attributes/Validator.php new file mode 100644 index 0000000..df78bc5 --- /dev/null +++ b/src/LightQL/Attributes/Validator.php @@ -0,0 +1,43 @@ + $validator The name of the validator class to use. + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +class Validator +{ + /** + * Initialize the validator attribute. + * + * @param class-string $validator The validator class name. + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly string $validator + ) { + if (empty($validator)) { + throw new InvalidArgumentException(self::class . ' must have a non-empty "validator" property'); + } + + if (!is_subclass_of($validator, IValidator::class)) { + throw new InvalidArgumentException("Validator class \"$validator\" must implement " . IValidator::class); + } + } +} diff --git a/src/LightQL/Entities/Column.php b/src/LightQL/Entities/Column.php index 0e34274..5ccee9e 100644 --- a/src/LightQL/Entities/Column.php +++ b/src/LightQL/Entities/Column.php @@ -14,6 +14,7 @@ use ElementaryFramework\LightQL\Attributes\OneToOne; use ElementaryFramework\LightQL\Attributes\Size; use ElementaryFramework\LightQL\Attributes\Unique; +use ElementaryFramework\LightQL\Attributes\Validator; use ReflectionException; /** @@ -25,6 +26,21 @@ */ class Column { + /** + * @var class-string The class name of the entity holding this column. + */ + private string $entityClass; + + /** + * The name of the property referenced by this column. + */ + private string $propertyName; + + /** + * The validator instance for this column. + */ + private ?IValidator $validator = null; + /** * The column name. */ @@ -99,7 +115,7 @@ class Column * @param array{int, int} $size The array of sizes containing (min, max) values only. * @param null|T $default The default value of the column. */ - public function __construct(string $name, string $type, array $size, mixed $default = null) + private function __construct(string $name, string $type, array $size, mixed $default = null) { $this->name = $name; $this->type = $type; @@ -116,8 +132,6 @@ public function __construct(string $name, string $type, array $size, mixed $defa * @param string $property The name of the property. * * @throws ReflectionException - * - * @internal */ public static function ofEntityProperty(string|object $entity, string $property): self { @@ -146,6 +160,31 @@ public static function ofEntityProperty(string|object $entity, string $property) $column->isOneToMany = AttributeUtils::propertyHasAttribute($entity, $property, OneToMany::class); $column->isOneToOne = AttributeUtils::propertyHasAttribute($entity, $property, OneToOne::class); + try { + $validator = AttributeUtils::ofProperty($entity, $property, Validator::class)->validator; + $column->validator = new $validator; + } catch (ReflectionException) { + $column->validator = null; + } + + $column->entityClass = is_string($entity) ? $entity : $entity::class; + $column->propertyName = $property; + return $column; } + + /** + * Validates the given value for this column if a validator was specified. + * + * @param mixed $value The value to validate. + * @param IEntity $entity The entity instance. + */ + public function validate(mixed $value, IEntity $entity): bool + { + if ($this->validator === null) { + return true; + } + + return $this->validator->validate($value, $this->propertyName, $entity); + } } diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 57d2736..99c3c26 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -12,6 +12,7 @@ use ElementaryFramework\LightQL\Exceptions\EntityException; use ElementaryFramework\LightQL\Exceptions\LightQLException; use ElementaryFramework\LightQL\Exceptions\QueryException; +use ElementaryFramework\LightQL\Exceptions\ValidationException; use ElementaryFramework\LightQL\LightQL; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; use Exception; @@ -164,11 +165,17 @@ public function persist(IEntity $entity): void } } - foreach ($columns as $column) { - $fieldAndValues[$column->name] = $entity->get($column->name); + foreach ($columns as $property => $column) { + $value = $entity->get($column->name); + + if (!$column->validate($value, $entity)) { + throw new ValidationException($property); + } + + $fieldAndValues[$column->name] = $value; } - $this->lightql->transaction(function () use ($entity, $fieldAndValues, $table, $autoIncrementProperty) { + try { $statement = $this->lightql ->builder() ->from($table->table) @@ -178,10 +185,9 @@ public function persist(IEntity $entity): void if ($autoIncrementProperty !== null) { $entity->$autoIncrementProperty = $statement->getLastInsertId(); } - - }, function (Throwable $e) { + } catch (Throwable $e) { throw new EntityException($e->getMessage()); - }); + } } /** @@ -189,8 +195,9 @@ public function persist(IEntity $entity): void * * @param IEntity $entity The entity to edit. * - * @throws EntityException + * @throws EntityException When the entity is not successfully saved in the database. * @throws ReflectionException + * @throws ValidationException When one of the entity's property value doesn't pass the assigned validator. */ public function merge(IEntity $entity): void { @@ -220,11 +227,17 @@ public function merge(IEntity $entity): void } } - foreach ($columns as $column) { - $fieldAndValues[$column->name] = $entity->get($column->name); + foreach ($columns as $property => $column) { + $value = $entity->get($column->name); + + if (!$column->validate($value, $entity)) { + throw new ValidationException($property); + } + + $fieldAndValues[$column->name] = $value; if ($column->isPrimaryKey) { - $where[$column->name] = $entity->get($column->name); + $where[$column->name] = $value; } } diff --git a/src/LightQL/Entities/IValidator.php b/src/LightQL/Entities/IValidator.php new file mode 100644 index 0000000..6354d5e --- /dev/null +++ b/src/LightQL/Entities/IValidator.php @@ -0,0 +1,29 @@ + Date: Wed, 12 Nov 2025 02:59:11 +0100 Subject: [PATCH 5/9] wip: Added support for value transformation. --- src/LightQL/Attributes/AutoIncrement.php | 2 +- src/LightQL/Attributes/Column.php | 21 +++++----- src/LightQL/Attributes/Id.php | 2 +- src/LightQL/Attributes/IdGenerator.php | 9 ++--- src/LightQL/Attributes/Index.php | 2 +- src/LightQL/Attributes/ManyToMany.php | 26 ++++++------ src/LightQL/Attributes/ManyToOne.php | 23 +++++------ src/LightQL/Attributes/NamedQuery.php | 12 +++--- src/LightQL/Attributes/NotNull.php | 2 +- src/LightQL/Attributes/OneToMany.php | 15 +++---- src/LightQL/Attributes/OneToOne.php | 28 ++++++------- src/LightQL/Attributes/PersistenceUnit.php | 11 +++-- src/LightQL/Attributes/Size.php | 14 +++---- src/LightQL/Attributes/Table.php | 12 +++--- src/LightQL/Attributes/Transformer.php | 42 +++++++++++++++++++ src/LightQL/Attributes/Unique.php | 6 +-- src/LightQL/Attributes/Validator.php | 9 ++--- src/LightQL/Entities/Column.php | 43 ++++++++++++++++++++ src/LightQL/Entities/Entity.php | 37 ++--------------- src/LightQL/Entities/EntityManager.php | 12 +++--- src/LightQL/Entities/IEntity.php | 21 ++++++---- src/LightQL/Entities/ITransformer.php | 47 ++++++++++++++++++++++ src/LightQL/Entities/IValidator.php | 8 ++-- src/LightQL/Sessions/Facade.php | 31 +++++++++----- 24 files changed, 268 insertions(+), 167 deletions(-) create mode 100644 src/LightQL/Attributes/Transformer.php create mode 100644 src/LightQL/Entities/ITransformer.php diff --git a/src/LightQL/Attributes/AutoIncrement.php b/src/LightQL/Attributes/AutoIncrement.php index b96f36a..fd61b9b 100644 --- a/src/LightQL/Attributes/AutoIncrement.php +++ b/src/LightQL/Attributes/AutoIncrement.php @@ -14,4 +14,4 @@ * This attribute should be used on a property with the {@see Column} attribute to have effect. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class AutoIncrement {} +readonly class AutoIncrement {} diff --git a/src/LightQL/Attributes/Column.php b/src/LightQL/Attributes/Column.php index 5177c51..c0a0674 100644 --- a/src/LightQL/Attributes/Column.php +++ b/src/LightQL/Attributes/Column.php @@ -11,28 +11,25 @@ * Column Attribute * * Used to mark a property as a table column. - * - * @property-read string $name The name of the column. - * @property-read string|null $type The type of the column. - * @property-read mixed $default The default value of the column. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class Column +readonly class Column { /** * Initialize the column attribute. * - * @param string $name The name of the column - * @param string|null $type The type of the column - * @param mixed $default The default value of the column + * @param string $name The name of the column. + * @param string|null $type The type of the column. + * @param mixed $default The default value of the column. * * @throws InvalidArgumentException */ public function __construct( - public readonly string $name, - public readonly ?string $type = null, - public readonly mixed $default = null - ) { + public string $name, + public ?string $type = null, + public mixed $default = null + ) + { if (empty($name)) { throw new InvalidArgumentException(self::class . " requires a non-empty \"name\" property"); } diff --git a/src/LightQL/Attributes/Id.php b/src/LightQL/Attributes/Id.php index 0ccaddf..f75b4c9 100644 --- a/src/LightQL/Attributes/Id.php +++ b/src/LightQL/Attributes/Id.php @@ -14,4 +14,4 @@ * This attribute should be used on a property with the {@see Column} attribute to have effect. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class Id {} +readonly class Id {} diff --git a/src/LightQL/Attributes/IdGenerator.php b/src/LightQL/Attributes/IdGenerator.php index a943a43..7398ab1 100644 --- a/src/LightQL/Attributes/IdGenerator.php +++ b/src/LightQL/Attributes/IdGenerator.php @@ -16,11 +16,9 @@ * This attribute should be used on a class with the {@see Table} attribute to have effect. * * @template TGenerator of IPrimaryKeyGenerator The generator class. - * - * @property-read class-string $generator Specify the class name to use as the ID generator of the current entity. */ #[Attribute(Attribute::TARGET_CLASS)] -class IdGenerator +readonly class IdGenerator { /** * Initialize the id generator attribute. @@ -30,8 +28,9 @@ class IdGenerator * @throws InvalidArgumentException */ public function __construct( - public readonly string $generator - ) { + public string $generator + ) + { if (empty($generator)) { throw new InvalidArgumentException(self::class . ' must have a non-empty "generator" property'); } diff --git a/src/LightQL/Attributes/Index.php b/src/LightQL/Attributes/Index.php index f86f185..e9bc3a2 100644 --- a/src/LightQL/Attributes/Index.php +++ b/src/LightQL/Attributes/Index.php @@ -25,7 +25,7 @@ * a class with the {@see Table} attribute to have effect. */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -class Index +readonly class Index { /** * The columns that form the index. diff --git a/src/LightQL/Attributes/ManyToMany.php b/src/LightQL/Attributes/ManyToMany.php index 17d615e..f8217db 100644 --- a/src/LightQL/Attributes/ManyToMany.php +++ b/src/LightQL/Attributes/ManyToMany.php @@ -15,41 +15,39 @@ * * This attribute should be used on a property with the {@see Column} attribute to have effect. * - * @property-read class-string $entity The referenced entity class name. - * @property-read class-string $pivotTable The name of the pivot table, or the class name of the pivot entity. - * @property-read string $foreignColumn The name of the column in the pivot table. - * @property-read string|null $localColumn The name of the column in the entity table. Set to null to use the primary key of the entity table. + * @template TEntity of IEntity The related entity class. + * @template TPivotEntity of IEntity The pivot entity class. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class ManyToMany +readonly class ManyToMany { /** * Initialize the many-to-many attribute. * - * @param class-string $entity The referenced entity class name. - * @param class-string $pivotTable The name of the pivot table, or the class name of the pivot entity. + * @param class-string $entity The referenced entity class name. + * @param class-string $pivotTable The class name of the pivot entity. * @param string $foreignColumn The name of the column in the pivot table. * @param string|null $localColumn The name of the column in the entity table. Set to null to use the primary key of the entity table. * * @throws InvalidArgumentException */ public function __construct( - public readonly string $entity, - public readonly string $pivotTable, - public readonly string $foreignColumn, - public readonly ?string $localColumn = null, + public string $entity, + public string $pivotTable, + public string $foreignColumn, + public ?string $localColumn = null, ) { if (empty($entity)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "entity" property'); } if (empty($pivotTable)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"pivotTable\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "pivotTable" property'); } if (empty($foreignColumn)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"foreignColumn\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "foreignColumn" property'); } } } diff --git a/src/LightQL/Attributes/ManyToOne.php b/src/LightQL/Attributes/ManyToOne.php index 948e8bf..54106c3 100644 --- a/src/LightQL/Attributes/ManyToOne.php +++ b/src/LightQL/Attributes/ManyToOne.php @@ -15,33 +15,32 @@ * * This attribute should be used on a property with the {@see Column} attribute to have effect. * - * @property-read class-string|string $entity The referenced entity class name or table name. - * @property-read string $referencedColumn The name of the referenced column. - * @property-read string|null $localColumn The name of the local column. If null, the primary key of the entity is used. + * @template TEntity of IEntity The related entity class. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class ManyToOne +readonly class ManyToOne { /** * Initialize the many-to-one attribute. * - * @param class-string|string $entity The referenced entity class name + * @param class-string $entity The referenced entity class name. * @param string|null $localColumn The name of the local column. If null, the primary key of the entity is used. - * @param string $referencedColumn The name of the referenced column + * @param string $referencedColumn The name of the referenced column. * * @throws InvalidArgumentException */ public function __construct( - public readonly string $entity, - public readonly string $referencedColumn, - public readonly ?string $localColumn = null, - ) { + public string $entity, + public string $referencedColumn, + public ?string $localColumn = null, + ) + { if (empty($entity)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "entity" property'); } if (empty($referencedColumn)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "referencedColumn" property'); } } } diff --git a/src/LightQL/Attributes/NamedQuery.php b/src/LightQL/Attributes/NamedQuery.php index 405f72a..136c63e 100644 --- a/src/LightQL/Attributes/NamedQuery.php +++ b/src/LightQL/Attributes/NamedQuery.php @@ -13,12 +13,9 @@ * Used to create named queries associated with an entity. * * This attribute should be used on a class with the {@see Table} attribute to have effect. - * - * @property-read string $name The query's name. - * @property-read string $query The SQL query. */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -class NamedQuery +readonly class NamedQuery { /** * Initialize the named query attribute. @@ -29,9 +26,10 @@ class NamedQuery * @throws InvalidArgumentException */ public function __construct( - public readonly string $name, - public readonly string $query - ) { + public string $name, + public string $query + ) + { if (empty($name)) { throw new InvalidArgumentException(self::class . ' requires a non-empty name property.'); } diff --git a/src/LightQL/Attributes/NotNull.php b/src/LightQL/Attributes/NotNull.php index b04fb64..a30e3c7 100644 --- a/src/LightQL/Attributes/NotNull.php +++ b/src/LightQL/Attributes/NotNull.php @@ -14,4 +14,4 @@ * This attribute should be used on a property with the {@see Column} attribute to have effect. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class NotNull {} +readonly class NotNull {} diff --git a/src/LightQL/Attributes/OneToMany.php b/src/LightQL/Attributes/OneToMany.php index a52ae45..de1b84e 100644 --- a/src/LightQL/Attributes/OneToMany.php +++ b/src/LightQL/Attributes/OneToMany.php @@ -15,13 +15,10 @@ * * This attribute should be used on a property with the {@see Column} attribute to have effect. * - * @template TEntity of IEntity - * - * @property-read class-string $entity The referenced entity class name. - * @property-read string $mappedBy The name of the property in the referenced entity that maps back to this entity. + * @template TEntity of IEntity The related entity class. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class OneToMany +readonly class OneToMany { /** * Initialize the one-to-many attribute. @@ -32,16 +29,16 @@ class OneToMany * @throws InvalidArgumentException */ public function __construct( - public readonly string $entity, - public readonly string $mappedBy + public string $entity, + public string $mappedBy ) { if (empty($entity)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "entity" property'); } if (empty($mappedBy)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"mappedBy\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "mappedBy" property'); } } } diff --git a/src/LightQL/Attributes/OneToOne.php b/src/LightQL/Attributes/OneToOne.php index 2c9d672..f83f380 100644 --- a/src/LightQL/Attributes/OneToOne.php +++ b/src/LightQL/Attributes/OneToOne.php @@ -5,6 +5,7 @@ namespace ElementaryFramework\LightQL\Attributes; use Attribute; +use ElementaryFramework\LightQL\Entities\IEntity; use InvalidArgumentException; /** @@ -14,45 +15,44 @@ * * This attribute should be used on a property with the {@see Column} attribute to have effect. * - * @property-read class-string|string $entity The referenced entity class name or table name. - * @property-read string|null $referencedColumn The name of the referenced column. - * @property-read string|null $mappedBy The name of the property in the referenced entity that maps this relation. + * @template TEntity of IEntity The related entity class. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class OneToOne +readonly class OneToOne { /** * Initialize the one-to-one attribute. * - * @param class-string|string $entity The referenced entity class name or table name. + * @param class-string $entity The referenced entity class name. * @param string|null $referencedColumn The name of the referenced column. * @param string|null $mappedBy The name of the property in the referenced entity that maps this relation. * * @throws InvalidArgumentException */ public function __construct( - public readonly string $entity, - public readonly ?string $referencedColumn = null, - public readonly ?string $mappedBy = null - ) { + public string $entity, + public ?string $referencedColumn = null, + public ?string $mappedBy = null + ) + { if (empty($entity)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"entity\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "entity" property'); } if ($referencedColumn === null && $mappedBy === null) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" or \"mappedBy\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "referencedColumn" or "mappedBy" property'); } if ($referencedColumn !== null && $mappedBy !== null) { - throw new InvalidArgumentException(self::class . " requires either a \"referencedColumn\" or \"mappedBy\" property, not both"); + throw new InvalidArgumentException(self::class . ' requires either a "referencedColumn" or "mappedBy" property, not both'); } if ($referencedColumn !== null && empty($referencedColumn)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"referencedColumn\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "referencedColumn" property'); } if ($mappedBy !== null && empty($mappedBy)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"mappedBy\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "mappedBy" property'); } } } diff --git a/src/LightQL/Attributes/PersistenceUnit.php b/src/LightQL/Attributes/PersistenceUnit.php index 8b61ca8..f2bbecd 100644 --- a/src/LightQL/Attributes/PersistenceUnit.php +++ b/src/LightQL/Attributes/PersistenceUnit.php @@ -11,11 +11,9 @@ * Persistence Unit Attribute * * Used to specify the persistence unit to use for an entity manager. - * - * @property-read string $name The name of this persistence unit. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class PersistenceUnit +readonly class PersistenceUnit { /** * Initialize the persistence unit attribute. @@ -25,10 +23,11 @@ class PersistenceUnit * @throws InvalidArgumentException */ public function __construct( - public readonly string $name - ) { + public string $name + ) + { if (empty($name)) { - throw new InvalidArgumentException(self::class . ' requires a non-empty name property.'); + throw new InvalidArgumentException(self::class . ' requires a non-empty "name" property.'); } } } diff --git a/src/LightQL/Attributes/Size.php b/src/LightQL/Attributes/Size.php index 5cdab5d..3665845 100644 --- a/src/LightQL/Attributes/Size.php +++ b/src/LightQL/Attributes/Size.php @@ -13,12 +13,9 @@ * Used to set various sizes for a mapped property. * * This attribute should be used on a property with the {@see Column} attribute to have effect. - * - * @property-read int|null $min The minimal size of the value. - * @property-read int|null $max The maximal size of the value. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class Size +readonly class Size { /** * Initialize the size attribute. @@ -29,11 +26,12 @@ class Size * @throws InvalidArgumentException */ public function __construct( - public readonly ?int $max = null, - public readonly ?int $min = null - ) { + public ?int $max = null, + public ?int $min = null + ) + { if ($max === null && $min === null) { - throw new InvalidArgumentException(self::class . ' requires a min and/or max property'); + throw new InvalidArgumentException(self::class . ' requires a "min" and/or "max" property'); } } } diff --git a/src/LightQL/Attributes/Table.php b/src/LightQL/Attributes/Table.php index 5bee8a6..a39f05b 100644 --- a/src/LightQL/Attributes/Table.php +++ b/src/LightQL/Attributes/Table.php @@ -12,12 +12,9 @@ * Entity Attribute * * Used to mark a class as an entity. - * - * @property-read string $table The name of the table represented by the entity. - * @property-read FetchMode $fetchMode The fetch mode used when querying the entity. */ #[Attribute(Attribute::TARGET_CLASS)] -class Table +readonly class Table { /** * Initialize the entity attribute. @@ -28,11 +25,12 @@ class Table * @throws InvalidArgumentException */ public function __construct( - public string $table, + public string $table, public FetchMode $fetchMode = FetchMode::LAZY - ) { + ) + { if (empty($table)) { - throw new InvalidArgumentException(self::class . " requires a non-empty \"table\" property"); + throw new InvalidArgumentException(self::class . ' requires a non-empty "table" property'); } } } diff --git a/src/LightQL/Attributes/Transformer.php b/src/LightQL/Attributes/Transformer.php new file mode 100644 index 0000000..cd10c0d --- /dev/null +++ b/src/LightQL/Attributes/Transformer.php @@ -0,0 +1,42 @@ + $transformer The transformer class name. + * + * @throws InvalidArgumentException + */ + public function __construct( + public string $transformer + ) + { + if (empty($transformer)) { + throw new InvalidArgumentException(self::class . ' must have a non-empty "transformer" property'); + } + + if (!is_subclass_of($transformer, ITransformer::class)) { + throw new InvalidArgumentException("Transformer class \"$transformer\" must implement " . ITransformer::class); + } + } +} diff --git a/src/LightQL/Attributes/Unique.php b/src/LightQL/Attributes/Unique.php index 8dfd1dd..15fc076 100644 --- a/src/LightQL/Attributes/Unique.php +++ b/src/LightQL/Attributes/Unique.php @@ -25,21 +25,21 @@ * a class with the {@see Table} attribute to have effect. */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -class Unique +readonly class Unique { /** * The columns that form the unique constraint. * * @var string[] */ - public readonly array $columns; + public array $columns; /** * The name of the unique constraint. * * @var string|null */ - public readonly ?string $name; + public ?string $name; /** * Initialize the unique attribute. diff --git a/src/LightQL/Attributes/Validator.php b/src/LightQL/Attributes/Validator.php index df78bc5..1aa9ec1 100644 --- a/src/LightQL/Attributes/Validator.php +++ b/src/LightQL/Attributes/Validator.php @@ -16,11 +16,9 @@ * This attribute should be used on a property with the {@see Column} attribute to have effect. * * @template TValidator of IValidator The validator class. - * - * @property-read class-string $validator The name of the validator class to use. */ #[Attribute(Attribute::TARGET_PROPERTY)] -class Validator +readonly class Validator { /** * Initialize the validator attribute. @@ -30,8 +28,9 @@ class Validator * @throws InvalidArgumentException */ public function __construct( - public readonly string $validator - ) { + public string $validator + ) + { if (empty($validator)) { throw new InvalidArgumentException(self::class . ' must have a non-empty "validator" property'); } diff --git a/src/LightQL/Entities/Column.php b/src/LightQL/Entities/Column.php index 5ccee9e..373e2f4 100644 --- a/src/LightQL/Entities/Column.php +++ b/src/LightQL/Entities/Column.php @@ -13,6 +13,7 @@ use ElementaryFramework\LightQL\Attributes\OneToMany; use ElementaryFramework\LightQL\Attributes\OneToOne; use ElementaryFramework\LightQL\Attributes\Size; +use ElementaryFramework\LightQL\Attributes\Transformer; use ElementaryFramework\LightQL\Attributes\Unique; use ElementaryFramework\LightQL\Attributes\Validator; use ReflectionException; @@ -41,6 +42,11 @@ class Column */ private ?IValidator $validator = null; + /** + * The transformer instance for this column. + */ + private ?ITransformer $transformer = null; + /** * The column name. */ @@ -167,6 +173,13 @@ public static function ofEntityProperty(string|object $entity, string $property) $column->validator = null; } + try { + $transformer = AttributeUtils::ofProperty($entity, $property, Transformer::class)->transformer; + $column->transformer = new $transformer; + } catch (ReflectionException) { + $column->transformer = null; + } + $column->entityClass = is_string($entity) ? $entity : $entity::class; $column->propertyName = $property; @@ -187,4 +200,34 @@ public function validate(mixed $value, IEntity $entity): bool return $this->validator->validate($value, $this->propertyName, $entity); } + + /** + * Serializes the given value for this column. + * + * @param mixed $value The value to serialize. + * @param IEntity $entity The entity instance. + */ + public function serialize(mixed $value, IEntity $entity): mixed + { + if ($this->transformer === null) { + return $value; + } + + return $this->transformer->serialize($value, $this->propertyName, $entity); + } + + /** + * Unserializes the given value for this column. + * + * @param mixed $value The value to unserialize. + * @param array $row The table row. + */ + public function unserialize(mixed $value, array $row): mixed + { + if ($this->transformer === null) { + return $value; + } + + return $this->transformer->unserialize($value, $this->name, $row); + } } diff --git a/src/LightQL/Entities/Entity.php b/src/LightQL/Entities/Entity.php index 2290f61..ba19b5c 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -37,12 +37,10 @@ abstract class Entity implements IEntity * * @var Column[] */ - private array $columns = []; + private(set) array $columns = []; /** - * Entity constructor. - * - * @param array $data The raw table row data. + * {@inheritDoc} * * @throws EntityException * @throws ReflectionException @@ -59,7 +57,7 @@ public function __construct(array $data = []) $pkFound = false; foreach ($properties as $property) { - if ($this->propertyHasAttribute($property->name, ColumnAttribute::class)) { + if (AttributeUtils::propertyHasAttribute($this, $property->name, ColumnAttribute::class)) { $column = Column::ofEntityProperty($this, $property->name); $this->columns[$property->name] = $column; @@ -90,11 +88,9 @@ public function hydrate(array $data): void if (!$column->isRelation) { if (array_key_exists($column->name, $this->raw)) { $this->{$property} = $this->raw[$column->name]; - } elseif ($this->{$property} === null) { + } elseif (!isset($this->{$property})) { $this->{$property} = $column->default; } - } else { - $this->{$property} = null; } } } @@ -145,31 +141,6 @@ public function set(string $column, $value): void $this->hydrate([$column => $value]); } - /** - * {@inheritDoc} - */ - public function getColumns(): array - { - return $this->columns; - } - - /** - * Checks if a property has the given attribute. - * - * @template TAttribute Attribute class. - * - * @param string $property The name of the property. - * @param class-string $annotation The name of the attribute. - * - * @return bool - * - * @throws ReflectionException - */ - private function propertyHasAttribute(string $property, string $annotation): bool - { - return AttributeUtils::propertyHasAttribute($this, $property, $annotation); - } - /** * Checks if the given column name exists in the raw values of this entity. * diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 99c3c26..08b8580 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -69,7 +69,7 @@ public function find(string $entityClass, string|int|IPrimaryKey $id): array /** @var IEntity $entity */ $entity = new $entityClass; - $columns = $entity->getColumns(); + $columns = $entity->columns; $where = []; @@ -113,7 +113,7 @@ public function persist(IEntity $entity): void { $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); + $columns = $entity->columns; $fieldAndValues = []; $autoIncrementProperty = null; @@ -172,7 +172,7 @@ public function persist(IEntity $entity): void throw new ValidationException($property); } - $fieldAndValues[$column->name] = $value; + $fieldAndValues[$column->name] = $column->serialize($value, $entity); } try { @@ -203,7 +203,7 @@ public function merge(IEntity $entity): void { $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); + $columns = $entity->columns; $fieldAndValues = []; $where = []; @@ -234,7 +234,7 @@ public function merge(IEntity $entity): void throw new ValidationException($property); } - $fieldAndValues[$column->name] = $value; + $fieldAndValues[$column->name] = $column->serialize($value, $entity); if ($column->isPrimaryKey) { $where[$column->name] = $value; @@ -265,7 +265,7 @@ public function delete(IEntity $entity): void { $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); + $columns = $entity->columns; $where = []; $pk = []; diff --git a/src/LightQL/Entities/IEntity.php b/src/LightQL/Entities/IEntity.php index d51d27c..5f6b9a1 100644 --- a/src/LightQL/Entities/IEntity.php +++ b/src/LightQL/Entities/IEntity.php @@ -12,6 +12,20 @@ */ interface IEntity { + /** + * @var array Gets the entity columns. + */ + public array $columns { + get; + } + + /** + * Create a new instance of an entity. + * + * @param array $row The raw table row data. + */ + public function __construct(array $row = []); + /** * Populates data in the entity. * @@ -33,11 +47,4 @@ public function get(string $column): mixed; * @param mixed $value The table column value. */ public function set(string $column, mixed $value): void; - - /** - * Gets the entity columns. - * - * @return array - */ - public function getColumns(): array; } diff --git a/src/LightQL/Entities/ITransformer.php b/src/LightQL/Entities/ITransformer.php new file mode 100644 index 0000000..b877d29 --- /dev/null +++ b/src/LightQL/Entities/ITransformer.php @@ -0,0 +1,47 @@ + $row The table row being read. + * + * @return TEntityValue The unserialized value. + */ + public function unserialize(mixed $value, string $column, array $row): mixed; +} \ No newline at end of file diff --git a/src/LightQL/Entities/IValidator.php b/src/LightQL/Entities/IValidator.php index 6354d5e..3881b68 100644 --- a/src/LightQL/Entities/IValidator.php +++ b/src/LightQL/Entities/IValidator.php @@ -7,12 +7,12 @@ use ElementaryFramework\LightQL\Attributes\Validator; /** - * IValueValidator + * IValidator * - * This interface provides a contract for implementations of values validators. Values validators are applied to entity - * columns through the {@see Validator} attribute. Only one validator is allowed on the same attribute. + * This interface provides a contract for implementations of values validators. Value validators are applied to entity + * columns through the {@see Validator} attribute. Only one validator is allowed on the same column. * - * @template TValue The value type being validated + * @template TValue The value type being validated. */ interface IValidator { diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index 0dae571..71a62d8 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -71,7 +71,7 @@ public function __construct(string|IEntity $class) ->getAttributes(PersistenceUnitAttribute::class); if (empty($propAttributes)) { - throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no PersistenceUnit attribute."); + throw new FacadeException('Cannot create the entity facade. The property "entityManager" has no PersistenceUnit attribute.'); } $reflection = new ReflectionClass($class); @@ -105,7 +105,7 @@ public function create(IEntity &$entity): void $this->entityManager->persist($entity); - $columns = $entity->getColumns(); + $columns = $entity->columns; foreach ($columns as $property => $column) { if ($column->isOneToMany) { $this->handleRelation($entity, $property, $this->fetchOneToMany(...)); @@ -147,7 +147,7 @@ public function delete(IEntity &$entity): void */ public function retrieve(int|string|IPrimaryKey $id): IEntity { - return $this->parseRawEntity( + return $this->parseRow( $this->entityManager->find($this->entityClass, $id), ); } @@ -167,7 +167,7 @@ public function list(): array ->execute() ->fetchAll(); - return $this->parseRawEntities($rawEntities); + return $this->parseRows($rawEntities); } /** @@ -186,7 +186,7 @@ public function range(int $start, int $length): array ->execute() ->fetchAll(); - return $this->parseRawEntities($rawEntities); + return $this->parseRows($rawEntities); } /** @@ -264,7 +264,7 @@ private function fetchManyToMany(IEntity $entity, string $property): array /** @var IEntity $referencedEntity */ $referencedEntity = new $manyToMany->entity; - foreach ($referencedEntity->getColumns() as $p => $c) { + foreach ($referencedEntity->columns as $p => $c) { if ($c->isManyToMany) { /** @var ManyToMany $mappedManyToMany */ $mappedManyToMany = new ReflectionProperty($referencedEntity, $p)->getAttributes(ManyToMany::class)[0]->newInstance(); @@ -478,10 +478,10 @@ private function resolveMappedPropertyName(string $entityClass, string $check, s * @throws EntityException * @throws ReflectionException */ - private function parseRawEntities(array $rawEntities): array + private function parseRows(array $rawEntities): array { return array_map( - fn(array $rawEntity) => $this->parseRawEntity($rawEntity), + fn(array $rawEntity) => $this->parseRow($rawEntity), $rawEntities ); } @@ -489,17 +489,26 @@ private function parseRawEntities(array $rawEntities): array /** * Parses raw data to Entity. * - * @param array $rawEntity Raw entity data provided from database. + * @param array $row Raw entity data provided from database. * * @return TEntity * * @throws EntityException * @throws ReflectionException */ - private function parseRawEntity(array $rawEntity): IEntity + private function parseRow(array $row): IEntity { /** @var TEntity $entity */ - $entity = new ($this->entityClass)($rawEntity); + $entity = new ($this->entityClass)(); + + // Unserialize values + foreach ($entity->columns as $column) { + if (array_key_exists($column->name, $row)) { + $row[$column->name] = $column->unserialize($row[$column->name], $row); + } + } + + $entity->hydrate($row); $properties = new ReflectionClass($this->entityClass)->getProperties(); From 274cdb3949f37dcb0419857c85d815b23602e203 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 03:34:19 +0100 Subject: [PATCH 6/9] wip: Allow to write persistence unit files in XML. --- src/LightQL/Persistence/PersistenceUnit.php | 88 +++++++++++++++++++-- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/src/LightQL/Persistence/PersistenceUnit.php b/src/LightQL/Persistence/PersistenceUnit.php index d47aa55..c4a7081 100644 --- a/src/LightQL/Persistence/PersistenceUnit.php +++ b/src/LightQL/Persistence/PersistenceUnit.php @@ -12,10 +12,10 @@ * * Configures parameters to use for database connection. */ -class PersistenceUnit +final class PersistenceUnit { /** - * The DBMS. + * The database management system (postgres, sqlite, mysql, etc.) */ private(set) DBMS $dbms; @@ -39,17 +39,22 @@ class PersistenceUnit */ private(set) string $password; + /** + * The database connection port. + */ + private(set) ?string $port = null; + /** * The list of registered persistence unit files. * * @var array */ - private static array $_registry = array(); + private static array $_registry = []; /** * @var list */ - private static array $_units = array(); + private static array $_units = []; /** * Registers a new persistence unit. @@ -85,15 +90,22 @@ private function __construct(string $key) $parts = explode(".", $filename); $extension = $parts[count($parts) - 1]; - $content = null; + $content = []; + if ($extension === "ini") { - $content = parse_ini_file(self::$_registry[$key]); + $content = $this->parseIniConfig($key); } elseif ($extension === "json") { - $content = json_decode(file_get_contents(self::$_registry[$key]), true); + $content = $this->parseJsonConfig($key); + } elseif ($extension === "xml") { + $content = $this->parseXmlConfig($key); } else { throw new PersistenceUnitException("Unsupported file type used to create persistence unit {$filename}."); } + if (empty($content)) { + throw new PersistenceUnitException("Unable to parse persistence unit configuration file '{$filename}'."); + } + if (array_key_exists("DBMS", $content)) { $this->dbms = DBMS::from($content["DBMS"]); } else { @@ -123,6 +135,10 @@ private function __construct(string $key) } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the Password value."); } + + if (array_key_exists("Port", $content)) { + $this->port = $content["Port"]; + } } else { throw new PersistenceUnitException('Unable to find the persistence unit with the key "' . $key . '". Have you registered this persistence unit?'); } @@ -142,4 +158,62 @@ public static function create(string $key): static ? self::$_units[$key] : ((self::$_units[$key] = new static($key))); } + + /** + * Parse a persistence unit in INI format. + * + * @param string $key The persistence unit key. + * @return array + */ + public function parseIniConfig(string $key): array + { + return parse_ini_file(self::$_registry[$key]) ?: []; + } + + /** + * Parse a persistence unit in JSON format. + * + * @param string $key The persistence unit key. + * @return array + */ + public function parseJsonConfig(string $key): array + { + return json_decode(file_get_contents(self::$_registry[$key]), true); + } + + /** + * Parse a persistence unit in XML format. + * + * @param string $key The persistence unit key. + * @return array + * + * @throws PersistenceUnitException + */ + public function parseXmlConfig(string $key): array + { + if (!extension_loaded("dom")) + return []; + + $content = []; + + $dom = new \DOMDocument("1.0", "utf-8"); + $dom->loadXML(file_get_contents(self::$_registry[$key])); + + if ($dom->documentElement->nodeName !== "PersistenceUnit") { + throw new PersistenceUnitException("Invalid persistence unit XML configuration file provided."); + } + + /** @var \DOMElement $node */ + foreach ($dom->documentElement->childNodes as $node) { + if ($node->nodeType === \XML_TEXT_NODE) { + continue; + } + + if (in_array($node->nodeName, ['DBMS', 'Hostname', 'DatabaseName', 'Username', 'Password', 'Port'])) { + $content[$node->nodeName] = $node->nodeValue; + } + } + + return $content; + } } From dda73fe2b503e1db7de18fafb5bc5127dd2d5062 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 04:29:39 +0100 Subject: [PATCH 7/9] wip: Unserialize values at hydratation time. --- src/LightQL/Entities/Entity.php | 33 +++++++++++++++++++------------- src/LightQL/Entities/IEntity.php | 12 ++++++++---- src/LightQL/Sessions/Facade.php | 11 +---------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/LightQL/Entities/Entity.php b/src/LightQL/Entities/Entity.php index ba19b5c..5ed2e03 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -19,18 +19,23 @@ abstract class Entity implements IEntity { /** - * The raw data provided from the database. + * The reflection class of this entity. * - * @var array + * @var ReflectionClass|null */ - protected array $raw = []; + private ?ReflectionClass $reflection = null; /** - * The reflection class of this entity. + * @var array Stores unserialized data for each column of the entity. + */ + private array $unserialized = []; + + /** + * The raw data provided from the database. * - * @var ReflectionClass|null + * @var array */ - private ?ReflectionClass $reflection = null; + protected array $raw = []; /** * The array of database columns of this entity. @@ -76,15 +81,17 @@ public function __construct(array $data = []) /** * {@inheritDoc} */ - public function hydrate(array $data): void + public function hydrate(array $row): void { // Merge values - foreach ($data as $name => $value) { - $this->raw[$name] = $value; - } + $this->raw = [...$this->raw, ...$row]; - // Populate @column properties + // Unserialize and populate columns foreach ($this->columns as $property => $column) { + if (array_key_exists($column->name, $row)) { + $this->unserialized[$column->name] = $column->unserialize($row[$column->name], $row); + } + if (!$column->isRelation) { if (array_key_exists($column->name, $this->raw)) { $this->{$property} = $this->raw[$column->name]; @@ -100,9 +107,9 @@ public function hydrate(array $data): void */ public function get(string $column): mixed { - // Try to get the raw value + // Try to get the value if ($this->exists($column)) { - return $this->raw[$column]; + return $this->unserialized[$column]; } // Try to get the property value diff --git a/src/LightQL/Entities/IEntity.php b/src/LightQL/Entities/IEntity.php index 5f6b9a1..34816c3 100644 --- a/src/LightQL/Entities/IEntity.php +++ b/src/LightQL/Entities/IEntity.php @@ -29,19 +29,23 @@ public function __construct(array $row = []); /** * Populates data in the entity. * - * @param array $data The raw database data. + * @param array $row The raw database data. */ - public function hydrate(array $data): void; + public function hydrate(array $row): void; /** - * Gets the raw value of a table column. + * Gets the value of a table column. + * + * The returned value is ensured to be transformed by the applied {@see ITransformer} if any. * * @param string $column The table column name. */ public function get(string $column): mixed; /** - * Sets the raw value of a table column. + * Sets the value of a table column. + * + * The given value will be transformed by the applied {@see ITransformer} if needed. * * @param string $column The table column name. * @param mixed $value The table column value. diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index 71a62d8..8a7b74c 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -499,16 +499,7 @@ private function parseRows(array $rawEntities): array private function parseRow(array $row): IEntity { /** @var TEntity $entity */ - $entity = new ($this->entityClass)(); - - // Unserialize values - foreach ($entity->columns as $column) { - if (array_key_exists($column->name, $row)) { - $row[$column->name] = $column->unserialize($row[$column->name], $row); - } - } - - $entity->hydrate($row); + $entity = new ($this->entityClass)($row); $properties = new ReflectionClass($this->entityClass)->getProperties(); From 29d48b4f56cfbf45ede72b7ac699ce053e6ef789 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 04:30:18 +0100 Subject: [PATCH 8/9] wip: Ensure config file exists before parsing. --- src/LightQL/Persistence/PersistenceUnit.php | 36 ++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/LightQL/Persistence/PersistenceUnit.php b/src/LightQL/Persistence/PersistenceUnit.php index c4a7081..c2de3b5 100644 --- a/src/LightQL/Persistence/PersistenceUnit.php +++ b/src/LightQL/Persistence/PersistenceUnit.php @@ -86,18 +86,24 @@ public static function purge(): void private function __construct(string $key) { if (array_key_exists($key, self::$_registry)) { - $filename = basename(self::$_registry[$key]); + $filepath = self::$_registry[$key]; + + if (!file_exists($filepath)) { + throw new PersistenceUnitException("The persistence unit file at the path '{$filepath}' cannot be found."); + } + + $filename = basename($filepath); $parts = explode(".", $filename); $extension = $parts[count($parts) - 1]; $content = []; if ($extension === "ini") { - $content = $this->parseIniConfig($key); + $content = $this->parseIniConfig($filepath); } elseif ($extension === "json") { - $content = $this->parseJsonConfig($key); + $content = $this->parseJsonConfig($filepath); } elseif ($extension === "xml") { - $content = $this->parseXmlConfig($key); + $content = $this->parseXmlConfig($filepath); } else { throw new PersistenceUnitException("Unsupported file type used to create persistence unit {$filename}."); } @@ -152,44 +158,44 @@ private function __construct(string $key) * * @throws PersistenceUnitException */ - public static function create(string $key): static + public static function create(string $key): self { return array_key_exists($key, self::$_units) ? self::$_units[$key] - : ((self::$_units[$key] = new static($key))); + : ((self::$_units[$key] = new self($key))); } /** * Parse a persistence unit in INI format. * - * @param string $key The persistence unit key. + * @param string $filepath The persistence unit file. * @return array */ - public function parseIniConfig(string $key): array + private function parseIniConfig(string $filepath): array { - return parse_ini_file(self::$_registry[$key]) ?: []; + return parse_ini_file($filepath) ?: []; } /** * Parse a persistence unit in JSON format. * - * @param string $key The persistence unit key. + * @param string $filepath The persistence unit file. * @return array */ - public function parseJsonConfig(string $key): array + private function parseJsonConfig(string $filepath): array { - return json_decode(file_get_contents(self::$_registry[$key]), true); + return json_decode(file_get_contents($filepath), true); } /** * Parse a persistence unit in XML format. * - * @param string $key The persistence unit key. + * @param string $filepath The persistence unit file. * @return array * * @throws PersistenceUnitException */ - public function parseXmlConfig(string $key): array + private function parseXmlConfig(string $filepath): array { if (!extension_loaded("dom")) return []; @@ -197,7 +203,7 @@ public function parseXmlConfig(string $key): array $content = []; $dom = new \DOMDocument("1.0", "utf-8"); - $dom->loadXML(file_get_contents(self::$_registry[$key])); + $dom->loadXML(file_get_contents($filepath)); if ($dom->documentElement->nodeName !== "PersistenceUnit") { throw new PersistenceUnitException("Invalid persistence unit XML configuration file provided."); From cf82722f1997b06e9ae3d04d79783de2fd525dd1 Mon Sep 17 00:00:00 2001 From: Axel Nana Date: Wed, 12 Nov 2025 04:42:51 +0100 Subject: [PATCH 9/9] wip: Return `null` when no item is found. --- src/LightQL/Entities/EntityManager.php | 4 ++-- src/LightQL/Sessions/Facade.php | 21 ++++++++++++--------- src/LightQL/Sessions/IFacade.php | 10 +++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 08b8580..1bc8476 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -57,13 +57,13 @@ public function __construct(private PersistenceUnit $persistenceUnit) * @param class-string $entityClass The class name of the entity to find. * @param string|int|IPrimaryKey $id The value of the primary key. * - * @return array Raw data from database. + * @return array|null Raw data from database or null if row not found. * * @throws LightQLException * @throws ReflectionException * @throws QueryException */ - public function find(string $entityClass, string|int|IPrimaryKey $id): array + public function find(string $entityClass, string|int|IPrimaryKey $id): ?array { $table = AttributeUtils::ofClass($entityClass, Table::class); diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index 8a7b74c..ea8160c 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -99,14 +99,13 @@ public function __construct(string|IEntity $class) /** * {@inheritdoc} */ - public function create(IEntity &$entity): void + public function create(IEntity $entity): void { $this->ensureEntityType($entity); $this->entityManager->persist($entity); - $columns = $entity->columns; - foreach ($columns as $property => $column) { + foreach ($entity->columns as $property => $column) { if ($column->isOneToMany) { $this->handleRelation($entity, $property, $this->fetchOneToMany(...)); } elseif ($column->isManyToOne) { @@ -122,7 +121,7 @@ public function create(IEntity &$entity): void /** * {@inheritdoc} */ - public function update(IEntity &$entity): void + public function update(IEntity $entity): void { $this->ensureEntityType($entity); @@ -132,7 +131,7 @@ public function update(IEntity &$entity): void /** * {@inheritdoc } */ - public function delete(IEntity &$entity): void + public function delete(IEntity $entity): void { $this->ensureEntityType($entity); @@ -144,12 +143,16 @@ public function delete(IEntity &$entity): void * {@inheritdoc} * * @throws ReflectionException + * @throws QueryException */ - public function retrieve(int|string|IPrimaryKey $id): IEntity + public function retrieve(int|string|IPrimaryKey $id): ?IEntity { - return $this->parseRow( - $this->entityManager->find($this->entityClass, $id), - ); + $row = $this->entityManager->find($this->entityClass, $id); + if ($row === null) { + return null; + } + + return $this->parseRow($row); } /** diff --git a/src/LightQL/Sessions/IFacade.php b/src/LightQL/Sessions/IFacade.php index 6393155..7a56971 100644 --- a/src/LightQL/Sessions/IFacade.php +++ b/src/LightQL/Sessions/IFacade.php @@ -29,7 +29,7 @@ interface IFacade * @throws EntityException * @throws LightQLException */ - function create(IEntity &$entity): void; + function create(IEntity $entity): void; /** * Edit an entity. @@ -40,7 +40,7 @@ function create(IEntity &$entity): void; * @throws EntityException * @throws LightQLException */ - function update(IEntity &$entity): void; + function update(IEntity $entity): void; /** * Delete an entity. @@ -51,19 +51,19 @@ function update(IEntity &$entity): void; * @throws EntityException * @throws LightQLException */ - function delete(IEntity &$entity): void; + function delete(IEntity $entity): void; /** * Find an entity. * * @param int|string|IPrimaryKey $id The id of the entity to find - * @return TEntity + * @return TEntity|null * * @throws FacadeException * @throws EntityException * @throws LightQLException */ - function retrieve(int|string|IPrimaryKey $id): IEntity; + function retrieve(int|string|IPrimaryKey $id): ?IEntity; /** * Find all entities.