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/AutoIncrementAnnotation.php b/src/LightQL/Annotations/AutoIncrementAnnotation.php deleted file mode 100644 index d4d41f7..0000000 --- a/src/LightQL/Annotations/AutoIncrementAnnotation.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; - -/** - * 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/ColumnAnnotation.php b/src/LightQL/Annotations/ColumnAnnotation.php deleted file mode 100644 index bb6282c..0000000 --- a/src/LightQL/Annotations/ColumnAnnotation.php +++ /dev/null @@ -1,92 +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; - -/** - * 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/IdAnnotation.php b/src/LightQL/Annotations/IdAnnotation.php deleted file mode 100644 index 0f3ef1d..0000000 --- a/src/LightQL/Annotations/IdAnnotation.php +++ /dev/null @@ -1,54 +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; - -/** - * 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/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/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/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/NamedQueryAnnotation.php b/src/LightQL/Annotations/NamedQueryAnnotation.php deleted file mode 100644 index f341773..0000000 --- a/src/LightQL/Annotations/NamedQueryAnnotation.php +++ /dev/null @@ -1,107 +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; - -/** - * 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/NotNullAnnotation.php b/src/LightQL/Annotations/NotNullAnnotation.php deleted file mode 100644 index 8c94217..0000000 --- a/src/LightQL/Annotations/NotNullAnnotation.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; -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/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/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/PersistenceUnitAnnotation.php b/src/LightQL/Annotations/PersistenceUnitAnnotation.php deleted file mode 100644 index b447e7d..0000000 --- a/src/LightQL/Annotations/PersistenceUnitAnnotation.php +++ /dev/null @@ -1,77 +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; - -/** - * 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/SizeAnnotation.php b/src/LightQL/Annotations/SizeAnnotation.php deleted file mode 100644 index 7fcdb78..0000000 --- a/src/LightQL/Annotations/SizeAnnotation.php +++ /dev/null @@ -1,105 +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; - -/** - * 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/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/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/Attributes/AutoIncrement.php b/src/LightQL/Attributes/AutoIncrement.php new file mode 100644 index 0000000..fd61b9b --- /dev/null +++ b/src/LightQL/Attributes/AutoIncrement.php @@ -0,0 +1,17 @@ + $generator The ID generator class name. + * + * @throws InvalidArgumentException + */ + public function __construct( + public 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/Attributes/Index.php b/src/LightQL/Attributes/Index.php new file mode 100644 index 0000000..e9bc3a2 --- /dev/null +++ b/src/LightQL/Attributes/Index.php @@ -0,0 +1,69 @@ +columns = $columns; + $this->name = $name; + } +} diff --git a/src/LightQL/Attributes/ManyToMany.php b/src/LightQL/Attributes/ManyToMany.php new file mode 100644 index 0000000..f8217db --- /dev/null +++ b/src/LightQL/Attributes/ManyToMany.php @@ -0,0 +1,53 @@ + $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 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'); + } + + 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/Attributes/ManyToOne.php b/src/LightQL/Attributes/ManyToOne.php new file mode 100644 index 0000000..54106c3 --- /dev/null +++ b/src/LightQL/Attributes/ManyToOne.php @@ -0,0 +1,46 @@ + $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 string $entity, + public string $referencedColumn, + public ?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/Attributes/NamedQuery.php b/src/LightQL/Attributes/NamedQuery.php new file mode 100644 index 0000000..136c63e --- /dev/null +++ b/src/LightQL/Attributes/NamedQuery.php @@ -0,0 +1,41 @@ + $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 string $entity, + public 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/Attributes/OneToOne.php b/src/LightQL/Attributes/OneToOne.php new file mode 100644 index 0000000..f83f380 --- /dev/null +++ b/src/LightQL/Attributes/OneToOne.php @@ -0,0 +1,58 @@ + $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 string $entity, + public ?string $referencedColumn = null, + public ?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/Attributes/PersistenceUnit.php b/src/LightQL/Attributes/PersistenceUnit.php new file mode 100644 index 0000000..f2bbecd --- /dev/null +++ b/src/LightQL/Attributes/PersistenceUnit.php @@ -0,0 +1,33 @@ + $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 new file mode 100644 index 0000000..15fc076 --- /dev/null +++ b/src/LightQL/Attributes/Unique.php @@ -0,0 +1,69 @@ +columns = $columns; + $this->name = $name; + } +} diff --git a/src/LightQL/Attributes/Validator.php b/src/LightQL/Attributes/Validator.php new file mode 100644 index 0000000..1aa9ec1 --- /dev/null +++ b/src/LightQL/Attributes/Validator.php @@ -0,0 +1,42 @@ + $validator The validator class name. + * + * @throws InvalidArgumentException + */ + public function __construct( + public 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 1a971a9..373e2f4 100644 --- a/src/LightQL/Entities/Column.php +++ b/src/LightQL/Entities/Column.php @@ -1,186 +1,233 @@ - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ +declare(strict_types=1); 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\Transformer; +use ElementaryFramework\LightQL\Attributes\Unique; +use ElementaryFramework\LightQL\Attributes\Validator; +use ReflectionException; + /** * 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 { + /** + * @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 transformer instance for this column. + */ + private ?ITransformer $transformer = null; + /** * 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; + private(set) bool $isAutoIncrement; /** - * Defines if the column is a - * primary key. - * - * @var bool + * Defines if the column is a primary key. */ - public $isPrimaryKey; + private(set) bool $isPrimaryKey; /** - * Defines if the column has the - * UNIQUE property. - * - * @var bool + * Defines if the column has the UNIQUE constraint. */ - public $isUniqueKey; + private(set) 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; + private(set) 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; + private(set) 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; + private(set) 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; + 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. * - * @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. + * @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 __construct(string $name, string $type, array $size, $default = null) + private function __construct(string $name, string $type, array $size, mixed $default = null) { - $this->_name = $name; - $this->_type = $type; - $this->_size = $size; - $this->_default = $default; + $this->name = $name; + $this->type = $type; + $this->size = $size; + $this->default = $default; } /** - * Returns the column's name. + * Create a new column from the metadata of the given entity property. + * + * @template TClass of object The entity class. * - * @return string + * @param class-string|TClass $entity The entity class name or instance. + * @param string $property The name of the property. + * + * @throws ReflectionException */ - public function getName(): string + public static function ofEntityProperty(string|object $entity, string $property): self { - return $this->_name; + $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); + + try { + $validator = AttributeUtils::ofProperty($entity, $property, Validator::class)->validator; + $column->validator = new $validator; + } catch (ReflectionException) { + $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; + + return $column; } /** - * Returns the column's type. + * Validates the given value for this column if a validator was specified. * - * @return string + * @param mixed $value The value to validate. + * @param IEntity $entity The entity instance. */ - public function getType(): string + public function validate(mixed $value, IEntity $entity): bool { - return $this->_type; + if ($this->validator === null) { + return true; + } + + return $this->validator->validate($value, $this->propertyName, $entity); } /** - * Returns the column's size. + * Serializes the given value for this column. * - * @return array + * @param mixed $value The value to serialize. + * @param IEntity $entity The entity instance. */ - public function getSize(): array + public function serialize(mixed $value, IEntity $entity): mixed { - return $this->_size; + if ($this->transformer === null) { + return $value; + } + + return $this->transformer->serialize($value, $this->propertyName, $entity); } /** - * Returns the column's default value. + * Unserializes the given value for this column. * - * @return mixed + * @param mixed $value The value to unserialize. + * @param array $row The table row. */ - public function getDefault() + public function unserialize(mixed $value, array $row): mixed { - return $this->_default; + 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 94d6ebf..5ed2e03 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -1,126 +1,71 @@ - * @copyright 2018 Aliens Group, Inc. - * @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. + * The reflection class of this entity. + * + * @var ReflectionClass|null */ - const FETCH_EAGER = 1; + private ?ReflectionClass $reflection = null; /** - * Fetch data in lazy mode. + * @var array Stores unserialized data for each column of the entity. */ - const FETCH_LAZY = 2; + private array $unserialized = []; /** * The raw data provided from the database. * - * @var array - */ - protected $raw = array(); - - /** - * The reflection class of this entity. - * - * @var \ReflectionClass + * @var array */ - private $_reflection = null; + protected array $raw = []; /** * The array of database columns of this entity. * * @var Column[] */ - private $_columns = array(); + private(set) array $columns = []; /** - * Entity constructor. - * - * @param array $data The raw database data. + * {@inheritDoc} * * @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); + if (AttributeUtils::propertyHasAttribute($this, $property->name, ColumnAttribute::class)) { + $column = Column::ofEntityProperty($this, $property->name); - $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'); - - $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,149 +79,83 @@ 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 $row): void { // Merge values - foreach ($data as $name => $value) { - $this->raw[$name] = $value; - } + $this->raw = [...$this->raw, ...$row]; + + // 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); + } - // 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(); + if (!$column->isRelation) { + if (array_key_exists($column->name, $this->raw)) { + $this->{$property} = $this->raw[$column->name]; + } elseif (!isset($this->{$property})) { + $this->{$property} = $column->default; } - } else { - $this->{$property} = null; } } } /** - * 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)) { - return $this->raw[$column]; + // Try to get the value + if ($this->exists($column)) { + return $this->unserialized[$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)); - } - - /** - * Gets the list of table columns - * of this entity. - * - * @return Column[] - */ - public function getColumns(): array - { - 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. - * - * @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. - * - * @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. - * - * @return IAnnotation|mixed - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - */ - private function _getMetadata(string $property, string $type, ?string $name = null, $default = null) - { - $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; + $this->hydrate([$column => $value]); } /** - * 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/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 3acd650..1bc8476 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -1,165 +1,124 @@ - * @copyright 2018 Aliens Group, Inc. - * @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\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\Exceptions\ValidationException; 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 - */ - private $_persistenceUnit; - - /** - * The LightQL instance used by this - * entity manager. - * - * @var LightQL + * The LightQL instance used by this entity manager. */ - 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( - array( - "dbms" => $this->_persistenceUnit->getDbms(), - "database" => $this->_persistenceUnit->getDatabase(), - "hostname" => $this->_persistenceUnit->getHostname(), - "username" => $this->_persistenceUnit->getUsername(), - "password" => $this->_persistenceUnit->getPassword() - ) + $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. + * @return array|null Raw data from database or null if row not found. * - * @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(); + $columns = $entity->columns; - $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(); } /** * 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 + * @throws EntityException|ReflectionException + * @throws Throwable */ - public function persist(Entity &$entity) + public function persist(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); - $fieldAndValues = array(); + $columns = $entity->columns; + $fieldAndValues = []; $autoIncrementProperty = null; $idProperty = null; - /** @var Column $column */ foreach ($columns as $property => $column) { if ($autoIncrementProperty === null && $column->isAutoIncrement) { $autoIncrementProperty = $property; @@ -168,59 +127,65 @@ public function persist(Entity &$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, IEntityIdGenerator::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 IEntityIdGenerator $idGenerator */ - $idGenerator = $idGeneratorClass->newInstance(); + /** @var IPrimaryKeyGenerator $idGenerator */ + $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())); + $value = $entity->get($column->name); + + if (!$column->validate($value, $entity)) { + throw new ValidationException($property); + } + + $fieldAndValues[$column->name] = $column->serialize($value, $entity); } - $this->_lightql->beginTransaction(); try { - $this->_lightql - ->from($entityAnnotation[0]->table) - ->insert($fieldAndValues); + $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(); - + } catch (Throwable $e) { throw new EntityException($e->getMessage()); } } @@ -228,33 +193,34 @@ 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 + * @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(Entity &$entity) + public function merge(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); - $fieldAndValues = array(); + $columns = $entity->columns; + $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; @@ -262,24 +228,27 @@ public function merge(Entity &$entity) } foreach ($columns as $property => $column) { - $fieldAndValues[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $value = $entity->get($column->name); + + if (!$column->validate($value, $entity)) { + throw new ValidationException($property); + } + + $fieldAndValues[$column->name] = $column->serialize($value, $entity); if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->name] = $value; } } - $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()); } } @@ -287,34 +256,32 @@ 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 + * @throws EntityException + * @throws ReflectionException */ - public function delete(Entity &$entity) + public function delete(IEntity $entity): void { - $entityAnnotation = Annotations::ofClass($entity, "@entity"); + $table = AttributeUtils::ofClass($entity, Table::class); - $columns = $entity->getColumns(); - $fieldAndValues = array(); + $columns = $entity->columns; - $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(); } @@ -324,28 +291,25 @@ 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->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()); } } @@ -358,6 +322,6 @@ public function delete(Entity &$entity) */ public function getLightQL(): LightQL { - return $this->_lightql; + return $this->lightql; } } diff --git a/src/LightQL/Entities/IEntity.php b/src/LightQL/Entities/IEntity.php index 0ecbb72..34816c3 100644 --- a/src/LightQL/Entities/IEntity.php +++ b/src/LightQL/Entities/IEntity.php @@ -1,72 +1,54 @@ - * @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 { + /** + * @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. * - * @param array $data The raw database data. + * @param array $row The raw database data. */ - function hydrate(array $data); + public function hydrate(array $row): void; /** - * Gets the raw value of a table column. + * Gets the value of a table column. * - * @param string $column The table column name. + * The returned value is ensured to be transformed by the applied {@see ITransformer} if any. * - * @return mixed + * @param string $column The table column name. */ - function get(string $column); + 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. + * @param mixed $value The table column value. */ - function set(string $column, $value); - + public function set(string $column, mixed $value): void; } 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 @@ + $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 new file mode 100644 index 0000000..3881b68 --- /dev/null +++ b/src/LightQL/Entities/IValidator.php @@ -0,0 +1,29 @@ + - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -namespace ElementaryFramework\LightQL\Entities; - -use ElementaryFramework\LightQL\Exceptions\QueryException; - -/** - * 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 \ReflectionClass $entity The managed entity reflection class instance. - */ - public function setEntity(\ReflectionClass $entity) - { - $this->_entityReflection = $entity; - } - - /** - * Sets the named query to execute. - * - * @param string $query The named query. - */ - public function setQuery(string $query) - { - $this->_namedQuery = $query; - } - - /** - * 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) - { - $this->_parameters[$name] = $value; - } - - /** - * 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 new file mode 100644 index 0000000..ac6fa1d --- /dev/null +++ b/src/LightQL/Entities/Relation.php @@ -0,0 +1,22 @@ + = 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( + 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/Exceptions/ValidationException.php b/src/LightQL/Exceptions/ValidationException.php new file mode 100644 index 0000000..571591f --- /dev/null +++ b/src/LightQL/Exceptions/ValidationException.php @@ -0,0 +1,31 @@ + - * @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\Persistence\PersistenceUnit; +use ElementaryFramework\LightQL\Query\Builder; +use PDO; +use PDOException; +use PDOStatement; +use Throwable; /** - * 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; - - /** - * The where clause. - * - * @var string - * @access private - */ - private $_where = null; - - /** - * The order clause. - * - * @var string - * @access private - */ - private $_order = null; - - /** - * The limit clause. - * - * @var string - * @access private */ - private $_limit = null; + private(set) ?PDO $pdo = null; /** - * The "group by" 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 $_group = null; - - /** - * The distinct clause - * - * @var bool - * @access private - */ - private $_distinct = false; + public function __construct( + 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(); - /** - * The computed query string. - * - * @var string - * @access private - */ - private $_queryString = null; + $this->connect($dsn, $commands); + } /** - * Class __constructor + * Factory method for backward compatibility with array options. * - * @param array $options The lists of options + * @param array $options * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException */ - public function __construct(array $options = null) + public static function fromArray(array $options): self { - if (!is_array($options)) { - return false; - } + $dbms = DBMS::tryFrom(strtolower($options['dbms'] ?? 'sqlite')) ?? DBMS::SQLITE; - $attr = array(); + if (isset($options['dsn']['driver'])) { + $driver = $options['dsn']['driver']; + unset($options['dsn']['driver']); - 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"]; + $dsnParts = []; + foreach ($options['dsn'] as $key => $value) { + $dsnParts[] = is_int($key) ? $value : "{$key}={$value}"; } - - 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"] - ); - } - - // 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; - - case "sqlite": - $this->_driver = "sqlite"; - $attr = array( - $options['database'] - ); - break; - } - } - - $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); + $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, + ); } - return $this; - } - - /** - * Closes a connection - * - * @return void - */ - public function close(): void - { - $this->_pdo = false; + 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, + ); } /** - * Connect to the database / Instantiate PDO + * Factory method used to create a new LightQL instance using a persistence unit. * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException When the connexion fails. + * @param PersistenceUnit $persistenceUnit The persistence unit. * - * @return void + * @throws LightQLException */ - private function _instantiate(): void + public static function fromPersistenceUnit(PersistenceUnit $persistenceUnit): self { - try { - $this->_pdo = new \PDO( - $this->_dsn, - $this->username, - $this->password, - $this->_options - ); - } catch (\PDOException $e) { - throw new LightQLException($e->getMessage()); - } + return new self( + database: $persistenceUnit->database, + hostname: $persistenceUnit->hostname, + username: $persistenceUnit->username, + password: $persistenceUnit->password, + dbms: $persistenceUnit->dbms, + ); } /** - * Gets the current query string. - * - * @return string + * Close the database connection. */ - public function getQueryString(): string + public function close(): void { - 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. + * Execute raw query. * - * @param string|array $condition SQL condition in valid format - * - * @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; + try { + $statement = $this->pdo->prepare($query, $options); + if ($statement === false) { + throw new LightQLException("Failed to prepare statement"); + } + return $statement; + } catch (PDOException $e) { + throw new LightQLException("Prepare failed: " . $e->getMessage()); + } } /** - * Add a distinct clause. - * - * @return \ElementaryFramework\LightQL\LightQL + * Get last insert ID. */ - public function distinct(): LightQL + public function lastInsertId(): int { - $this->_distinct = true; - return $this; + return (int)$this->pdo->lastInsertId(); } /** - * 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 + * Quote a value. */ - public function select($columns = "*"): \PDOStatement + public function quote(string $value): string { - return $this->_select($columns); + return $this->pdo->quote($value); } /** - * 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). + * Execute the given callback in a transaction, and automatically commit on success. * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @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. * - * @return \PDOStatement + * @throws Throwable */ - private function _select($columns): \PDOStatement + public function transaction(callable $callback, ?callable $onError = null): void { - // Constructing the fields list - if (is_array($columns)) { - $_fields = ""; + $this->beginTransaction(); - foreach ($columns as $column => $alias) { - if (is_int($column)) { - $_fields .= "{$alias}, "; - } elseif (is_string($column)) { - $_fields .= "{$column} AS {$alias}, "; + try { + $callback(); + $this->commit(); + } catch (Throwable $e) { + $shouldCommit = false; + $lastException = $e; + + try { + if ($onError !== null) { + $shouldCommit = $onError($e); } + } catch (Throwable $e) { + $lastException = $e; } - $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(); + if ($shouldCommit) { + $this->commit(); + } else { + $this->rollBack(); + } - return $getFieldsData; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); + throw $lastException; } } /** - * Prepares a query. - * - * @param string $query The query to execute - * @param array $options PDO options - * - * @uses \PDO::prepare() - * - * @return \PDOStatement + * Begin transaction. */ - public function prepare(string $query, array $options = array()): \PDOStatement - { - return $this->_pdo->prepare($query, $options); - } - - /** - * Reset all clauses. - */ - protected function resetClauses() - { - $this->_distinct = false; - $this->_where = null; - $this->_order = null; - $this->_limit = null; - $this->_group = null; - } - - /** - * 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 - */ - 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. - * - * @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 + * Gets the error message from the last query, if any. * - * @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; - - if (is_array($columns)) { - $columns = implode(",", $columns); - } + $driver = $this->dbms->getDriver(); - 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(); - - while ($r = $join->fetch(\PDO::FETCH_OBJ)) { - $result[] = $r; - } + $attr = [ + 'host' => $this->hostname, + 'dbname' => $this->database, + ]; - return $result; - } - - /** - * 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 - */ - public function count($columns = "*") - { - if (is_array($columns)) { - $column = implode(",", $columns); + if ($this->port !== null) { + $attr['port'] = $this->port; } - $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}" : " ")); - - $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]); - } + return $attr; } /** - * 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 + * @return array */ - public function insert(array $fieldsAndValues): bool + private function buildSybaseDsnAttributes(): array { - $columns = array(); - $values = array(); + $attr = [ + 'host' => $this->hostname, + 'dbname' => $this->database, + ]; - 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); - - if (is_array($fieldsAndValues)) { - foreach ($fieldsAndValues as $column => $value) { - $count--; - $updates .= "{$column} = " . $this->parseValue($value); - $updates .= ($count != 0) ? ", " : ""; - } - } else { - $updates = $fieldsAndValues; - } - - $this->_queryString = trim("UPDATE {$this->table} SET {$updates}" . ((null !== $this->_where) ? " WHERE {$this->_where}" : "")); - - $getFieldsData = $this->prepare($this->_queryString); + // MSSQL can use dblib or sqlsrv driver + $useDblib = false; // Can be configured if needed - if ($getFieldsData->execute() !== false) { - $this->resetClauses(); - return true; - } else { - throw new LightQLException($getFieldsData->errorInfo()[2]); + if ($useDblib) { + $driver = 'dblib'; + return [ + 'host' => $this->hostname . ($this->port ? ":{$this->port}" : ''), + 'dbname' => $this->database, + ]; } - } - - /** - * Deletes data in table. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - * - * @return boolean - */ - public function delete(): bool - { - $this->_queryString = trim("DELETE FROM {$this->table}" . ((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, + ]; } /** - * Truncates a table. - * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * Get initialization commands for the connection. * - * @return boolean + * @return array */ - public function truncate(): bool + private function getInitCommands(): array { - $this->_queryString = "TRUNCATE {$this->table}"; - - $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); } - } - - /** - * 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); - } - - /** - * Gets the last inserted id by an - * INSERT query. - * - * @uses \PDO::lastInsertId() - * - * @return int - */ - public function lastInsertID(): int - { - 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(); + return $commands; } /** - * Commit changes made during a transaction. - * - * @uses \PDO::commit() + * Establish database connection. * - * @return bool + * @param array $initCommands + * @throws LightQLException */ - public function commit(): bool + private function connect(string $dsn, array $initCommands): void { - 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..c2de3b5 100644 --- a/src/LightQL/Persistence/PersistenceUnit.php +++ b/src/LightQL/Persistence/PersistenceUnit.php @@ -1,105 +1,68 @@ - * @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 +final class PersistenceUnit { /** - * The DBMS. - * - * @var string + * The database management system (postgres, sqlite, mysql, etc.) */ - 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 database connection port. + */ + private(set) ?string $port = null; /** * The list of registered persistence unit files. * - * @var array + * @var array */ - private static $_registry = array(); + private static array $_registry = []; /** - * @var PersistenceUnit[] + * @var list */ - private static $_units = array(); + private static array $_units = []; /** * 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 +70,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 = []; } /** @@ -123,48 +86,65 @@ public static function purge() 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 = null; + $content = []; + if ($extension === "ini") { - $content = parse_ini_file(self::$_registry[$key]); + $content = $this->parseIniConfig($filepath); } elseif ($extension === "json") { - $content = json_decode(file_get_contents(self::$_registry[$key]), true); + $content = $this->parseJsonConfig($filepath); + } elseif ($extension === "xml") { + $content = $this->parseXmlConfig($filepath); } 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 = $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."); } + + 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?'); } @@ -174,65 +154,72 @@ 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 + * @throws PersistenceUnitException */ - public function getDbms() + public static function create(string $key): self { - return $this->_dbms; + return array_key_exists($key, self::$_units) + ? self::$_units[$key] + : ((self::$_units[$key] = new self($key))); } /** - * Returns the database name. + * Parse a persistence unit in INI format. * - * @return string + * @param string $filepath The persistence unit file. + * @return array */ - public function getDatabase(): string + private function parseIniConfig(string $filepath): array { - return $this->_database; + return parse_ini_file($filepath) ?: []; } /** - * Returns the database server name. + * Parse a persistence unit in JSON format. * - * @return string + * @param string $filepath The persistence unit file. + * @return array */ - public function getHostname(): string + private function parseJsonConfig(string $filepath): array { - return $this->_hostname; + return json_decode(file_get_contents($filepath), true); } /** - * Returns the password of the user. + * Parse a persistence unit in XML format. * - * @return string - */ - public function getPassword(): string - { - return $this->_password; - } - - /** - * Returns the username. + * @param string $filepath The persistence unit file. + * @return array * - * @return string + * @throws PersistenceUnitException */ - public function getUsername(): string + private function parseXmlConfig(string $filepath): array { - return $this->_username; + if (!extension_loaded("dom")) + return []; + + $content = []; + + $dom = new \DOMDocument("1.0", "utf-8"); + $dom->loadXML(file_get_contents($filepath)); + + 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; } } diff --git a/src/LightQL/Query/Builder.php b/src/LightQL/Query/Builder.php new file mode 100644 index 0000000..6d98e91 --- /dev/null +++ b/src/LightQL/Query/Builder.php @@ -0,0 +1,804 @@ + + */ + 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/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 new file mode 100644 index 0000000..724e102 --- /dev/null +++ b/src/LightQL/Query/PendingQuery.php @@ -0,0 +1,110 @@ + $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..ea8160c 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -1,303 +1,232 @@ - * @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\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\Query; +use ElementaryFramework\LightQL\Entities\IPrimaryKey; +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 ElementaryFramework\LightQL\Query\NamedQuery; +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(PersistenceUnitAttribute::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 PersistenceUnitAttribute $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); + + foreach ($entity->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. - * - * @param mixed $id The id of the entity to find + * {@inheritdoc} * - * @return Entity - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws ReflectionException + * @throws QueryException */ - public function find($id): Entity + public function retrieve(int|string|IPrimaryKey $id): ?IEntity { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); + $row = $this->entityManager->find($this->entityClass, $id); + if ($row === null) { + return null; + } - return $this->_parseRawEntity( - $this->entityManager->find($this->getEntityClassName(), $id), - $annotations - ); + return $this->parseRow($row); } /** - * 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->parseRows($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->parseRows($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. * * @param string $name The name of the query. * - * @return Query + * @return NamedQuery * * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException + * @throws ReflectionException */ - public function getNamedQuery(string $name): Query + public function getNamedQuery(string $name): NamedQuery { - 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(NamedQueryAttribute::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 NamedQueryAttribute $namedQuery */ + foreach ($attributes as $namedQuery) { if ($namedQuery->name === $name) { $query = $namedQuery->query; break; @@ -305,39 +234,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 NamedQuery($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; - foreach ($referencedEntity->getColumns() as $p => $c) { + /** @var IEntity $referencedEntity */ + $referencedEntity = new $manyToMany->entity; + foreach ($referencedEntity->columns 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 +280,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}\"."); - } - - $mappedPropertyManyToOneAnnotation = Annotations::ofProperty($oneToMany[0]->entity, $mappedPropertyName, "@manyToOne"); - - $lightql = $this->entityManager->getLightQL(); - - $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}.*"); + /** @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(); - $className = $oneToMany[0]->entity; + $mappedPropertyName = $oneToMany->mappedBy; - $entity->{$property} = $result; + /** @var ManyToOne $mappedPropertyManyToOneAnnotation */ + $mappedPropertyManyToOneAnnotation = new ReflectionProperty($oneToMany->entity, $mappedPropertyName)->getAttributes(ManyToOne::class)[0]->newInstance(); - 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 +474,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 parseRows(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->parseRow($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 $row 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 parseRow(array $row): 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)($row); + + $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..7a56971 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|null * - * @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