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