diff --git a/README.md b/README.md index a441b4d..24f4abd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +
+ +![Banner](./assets/banner.png) + +
+ # LightQL The lightweight PHP ORM diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000..0319889 Binary files /dev/null and b/assets/banner.png differ diff --git a/bin/light-ql b/bin/light-ql new file mode 100644 index 0000000..7d3ffd6 --- /dev/null +++ b/bin/light-ql @@ -0,0 +1,71 @@ +#!/usr/bin/env php + + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +foreach (array(__DIR__ . '/../../../autoload.php', __DIR__ . '/../../vendor/autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) { + if (file_exists($file)) { + define('COMPOSER_INSTALL', $file); + break; + } +} + +unset($file); + +if (!defined('COMPOSER_INSTALL')) { + fwrite( + STDERR, + 'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL . + ' composer install' . PHP_EOL . PHP_EOL . + 'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL + ); + + die(1); +} + +require COMPOSER_INSTALL; + +use ElementaryFramework\LightQL\CLI\Commands\GenerateDatabaseCommand; +use ElementaryFramework\LightQL\CLI\Commands\GenerateEntitiesCommand; +use Symfony\Component\Console\Application; + +// Create the application +$app = new Application('A database migration tool and entities generator for LightQL', '0.0.1'); + +// Add commands +$app->addCommands([ + new GenerateEntitiesCommand(), + new GenerateDatabaseCommand() +]); + +// Run the app +$app->run(); diff --git a/composer.json b/composer.json index 56e22e7..f706b79 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,9 @@ "name": "elementaryframework/light-ql", "description": "The lightweight PHP ORM", "type": "library", + "bin": [ + "bin/light-ql" + ], "require-dev": { "phpunit/phpunit": "^7" }, @@ -14,7 +17,10 @@ ], "require": { "php": "^7.1.10", - "elementaryframework/annotations": "^2.0.1" + "ext-pdo": "*", + "elementaryframework/annotations": "^2.0.2", + "nette/php-generator": "^3.2", + "symfony/console": "^4.2" }, "keywords": [ "orm", diff --git a/src/LightQL/Annotations/ListenerAnnotation.php b/src/LightQL/Annotations/ListenerAnnotation.php new file mode 100644 index 0000000..46bae0c --- /dev/null +++ b/src/LightQL/Annotations/ListenerAnnotation.php @@ -0,0 +1,102 @@ + + * @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\Exceptions\AnnotationException; +use ElementaryFramework\Annotations\IAnnotationFileAware; + +/** + * Listener Annotation + * + * Used to define the listener for entity facades. + * + * @usage('class' => true, 'inherited' => true) + * + * @category Annotations + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/ListenerAnnotation + */ +class ListenerAnnotation extends Annotation implements IAnnotationFileAware +{ + /** + * Specify the class name to use as the listener + * of the current entity facade. + * + * @var string + */ + public $listener; + + /** + * 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("listener")); + + parent::initAnnotation($properties); + + if (!isset($this->listener)) { + throw new AnnotationException(self::class . " must have a \"listener\" property"); + } + + $this->listener = $this->file->resolveType($this->listener); + } + + /** + * 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/PkClassAnnotation.php b/src/LightQL/Annotations/PkClassAnnotation.php new file mode 100644 index 0000000..60c416a --- /dev/null +++ b/src/LightQL/Annotations/PkClassAnnotation.php @@ -0,0 +1,108 @@ + + * @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\Exceptions\AnnotationException; +use ElementaryFramework\Annotations\IAnnotationFileAware; + +/** + * Pk Class Annotation + * + * Used to define the primary key class 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/PkClassAnnotation + */ +class PkClassAnnotation extends Annotation implements IAnnotationFileAware +{ + /** + * Specify the class name to use as the primary key class + * of the current entity. + * + * @var string + */ + public $name; + + /** + * 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("name")); + + parent::initAnnotation($properties); + + if (!isset($this->name)) { + throw new AnnotationException(self::class . " must have a \"name\" property"); + } + + $this->name = $this->file->resolveType($this->name); + } + + /** + * 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/TransformerAnnotation.php b/src/LightQL/Annotations/TransformerAnnotation.php new file mode 100644 index 0000000..92fea5d --- /dev/null +++ b/src/LightQL/Annotations/TransformerAnnotation.php @@ -0,0 +1,105 @@ + + * @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; + +/** + * Transformer Annotation + * + * Used to define the value transformer of an entity. + * + * This annotation have to be associated with the @entity + * annotation to take effect. + * + * @usage('class' => true, 'inherited' => true) + * + * @category Annotations + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/TransformerAnnotation + */ +class TransformerAnnotation extends Annotation implements IAnnotationFileAware +{ + /** + * Specify the class name to use as the value transformer + * of the current entity. + * + * @var string + */ + public $transformer; + + /** + * 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("transformer")); + + parent::initAnnotation($properties); + + if (!isset($this->transformer)) { + throw new AnnotationException(self::class . " must have a \"transformer\" property"); + } + + $this->transformer = $this->file->resolveType($this->transformer); + } + + /** + * 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/ValidatorAnnotation.php b/src/LightQL/Annotations/ValidatorAnnotation.php new file mode 100644 index 0000000..cfea748 --- /dev/null +++ b/src/LightQL/Annotations/ValidatorAnnotation.php @@ -0,0 +1,105 @@ + + * @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; + +/** + * Validator Annotation + * + * Used to define the value validator of an entity. + * + * This annotation have to be associated with the @entity + * annotation to take effect. + * + * @usage('class' => true, 'inherited' => true) + * + * @category Annotations + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Annotations/ValidatorAnnotation + */ +class ValidatorAnnotation extends Annotation implements IAnnotationFileAware +{ + /** + * Specify the class name to use as the ID generator + * of the current entity. + * + * @var string + */ + public $validator; + + /** + * 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("validator")); + + parent::initAnnotation($properties); + + if (!isset($this->validator)) { + throw new AnnotationException(self::class . " must have a \"validator\" property"); + } + + $this->validator = $this->file->resolveType($this->validator); + } + + /** + * 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/CLI/Commands/GenerateDatabaseCommand.php b/src/LightQL/CLI/Commands/GenerateDatabaseCommand.php new file mode 100644 index 0000000..24d3e42 --- /dev/null +++ b/src/LightQL/CLI/Commands/GenerateDatabaseCommand.php @@ -0,0 +1,67 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\CLI\Commands; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class GenerateDatabaseCommand + */ +class GenerateDatabaseCommand extends Command +{ + const ARGUMENT_PERSISTENCE_UNIT = "persistence-unit"; + const ARGUMENT_PERSISTENCE_UNIT_SHORT = "p"; + + const ARGUMENT_INPUT_DIR = "input"; + const ARGUMENT_INPUT_DIR_SHORT = "i"; + + public function __construct() + { + parent::__construct("generate:database"); + } + + public function configure() + { + $this + ->setDescription("Generate database from LightQL entities") + ->addOption(self::ARGUMENT_PERSISTENCE_UNIT, self::ARGUMENT_PERSISTENCE_UNIT_SHORT, InputOption::VALUE_REQUIRED, "The path to the LightQL persistence unit file.") + ->addOption(self::ARGUMENT_INPUT_DIR, self::ARGUMENT_INPUT_DIR_SHORT, InputOption::VALUE_REQUIRED, "The path to the directory in which LightQL entities resides."); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + } +} \ No newline at end of file diff --git a/src/LightQL/CLI/Commands/GenerateEntitiesCommand.php b/src/LightQL/CLI/Commands/GenerateEntitiesCommand.php new file mode 100644 index 0000000..012452a --- /dev/null +++ b/src/LightQL/CLI/Commands/GenerateEntitiesCommand.php @@ -0,0 +1,344 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\CLI\Commands; + +use ElementaryFramework\LightQL\CLI\Utils\EntityFilePrinter; +use ElementaryFramework\LightQL\Entities\Entity; +use ElementaryFramework\LightQL\LightQL; +use ElementaryFramework\LightQL\Persistence\PersistenceUnit; +use ElementaryFramework\LightQL\Entities\IPrimaryKey; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Exception\InvalidOptionException; + +/** + * Class GenerateEntitiesCommand + */ +class GenerateEntitiesCommand extends Command +{ + const OPTION_FROM_DATABASE = "from-database"; + const OPTION_FROM_DATABASE_SHORT = "D"; + + const OPTION_FROM_SCHEMA = "from-schema"; + const OPTION_FROM_SCHEMA_SHORT = "S"; + + const OPTION_PERSISTENCE_UNIT = "persistence-unit"; + const OPTION_PERSISTENCE_UNIT_SHORT = "p"; + + const OPTION_OUTPUT_DIR = "output"; + const OPTION_OUTPUT_DIR_SHORT = "o"; + + const OPTION_NAMESPACE = "namespace"; + + /** + * @var LightQL + */ + private $_light; + + /** + * @var PersistenceUnit + */ + private $_pu; + + private $_properties; + + public function __construct() + { + parent::__construct("generate:entities"); + $this->_properties = array(); + } + + public function configure() + { + $this + ->setDescription("Generate LightQL entities") + ->addOption(self::OPTION_FROM_DATABASE, self::OPTION_FROM_DATABASE_SHORT, InputOption::VALUE_NONE, "Generate entities from a database", null) + ->addOption(self::OPTION_FROM_SCHEMA, self::OPTION_FROM_SCHEMA_SHORT, InputOption::VALUE_NONE, "Generate entities from a LightQL database schema", null) + ->addOption(self::OPTION_PERSISTENCE_UNIT, self::OPTION_PERSISTENCE_UNIT_SHORT, InputOption::VALUE_OPTIONAL, "The path to the LightQL persistence unit file.") + ->addOption(self::OPTION_OUTPUT_DIR, self::OPTION_OUTPUT_DIR_SHORT, InputOption::VALUE_OPTIONAL, "The path to the directory in which generated files will output.", ".") + ->addOption(self::OPTION_NAMESPACE, null, InputOption::VALUE_OPTIONAL, "The namespace of entity classes.", false); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + if ($input->getOption(self::OPTION_FROM_DATABASE) !== false && $input->getOption(self::OPTION_FROM_SCHEMA) !== false) + throw new InvalidOptionException("You have to choose to generate entities either from a database or from a schema."); + elseif ($input->getOption(self::OPTION_FROM_DATABASE) !== false || ($input->getOption(self::OPTION_FROM_DATABASE) === false && $input->getOption(self::OPTION_FROM_SCHEMA) === false)) + $this->_generateFromDatabase($input, $output); + elseif ($input->getOption(self::OPTION_FROM_SCHEMA) !== false) + $this->_generateFromSchema($input, $output); + else throw new InvalidOptionException("You have to choose to generate entities either from a database or from a schema."); + } + + public function _generateFromDatabase(InputInterface &$input, OutputInterface &$output) + { + // Register the defined persistence unit + PersistenceUnit::register(self::OPTION_PERSISTENCE_UNIT_SHORT, $input->getOption(self::OPTION_PERSISTENCE_UNIT)); + + // Create the PU + $this->_pu = PersistenceUnit::create(self::OPTION_PERSISTENCE_UNIT_SHORT); + + // Create a LightQL instance + $this->_light = new LightQL( + array( + "dbms" => $this->_pu->getDbms(), + "database" => $this->_pu->getDatabase(), + "hostname" => $this->_pu->getHostname(), + "username" => $this->_pu->getUsername(), + "password" => $this->_pu->getPassword(), + "port" => $this->_pu->getPort() + ) + ); + + // Get all tables of database + $results = $this->_light->from("information_schema.tables") + ->where(array("table_schema" => $this->_light->quote($this->_pu->getDatabase()))) + ->selectArray("table_name"); + + $i = 0; + + $output->writeln("Generating entities..."); + + foreach ($results as $table) { + // Generate entity + $this->_generateEntity($table["table_name"], $input, $output); + $i++; + } + + $output->writeln("Successfully generated {$i} entities from {$i} tables." . PHP_EOL); + } + + public function _generateFromSchema(InputInterface &$input, OutputInterface &$output) + { + } + + private function _generateEntity(string $name, InputInterface $input, OutputInterface $output) + { + // Create file + $file = (new \Nette\PhpGenerator\PhpFile) + ->addComment("THIS FILE IS GENERATED BY LIGHTQL" . PHP_EOL) + ->addComment("LightQL entity class for the table \"{$name}\"" . PHP_EOL) + ->addComment("Generated at " . date("Y-m-d H:i:s")); + + $class = null; + $namespace = null; + $className = $this->_generateObjectName($name); + + $pkFile = null; + $pkClass = null; + $pKeys = array(); + $pkClassName = $className . "PK"; + + if ($input->getOption(self::OPTION_NAMESPACE) !== false) { + $namespace = $file->addNamespace($input->getOption(self::OPTION_NAMESPACE)); + $namespace->addUse(Entity::class); + $class = $namespace->addClass($className); + } else { + $file->addUse(Entity::class); + $class = $file->addClass($className); + } + + if (!array_key_exists($className, $this->_properties)) + $this->_properties[$className] = array(); + + $class + ->setExtends(Entity::class) + ->addComment("Entity {$className}\n") + ->addComment("@entity('{$name}')") + ->addComment("@namedQuery('findAll', 'SELECT * FROM {$name}')"); + + // Get all columns of database + $results = $this->_light->from("information_schema.columns") + ->where( + array( + "table_schema" => $this->_light->quote($this->_pu->getDatabase()), + "table_name" => $this->_light->quote($name), + ) + ) + ->selectArray(); + + foreach ($results as $result) { + $colName = $result["COLUMN_NAME"]; + $colType = $result["DATA_TYPE"]; + $colDefault = $result["COLUMN_DEFAULT"]; + + $propertyName = $this->_generateObjectName($colName); + + // Get tables keys properties + $keys_results = $this->_light->from("information_schema.key_column_usage") + ->where( + array( + "information_schema.table_constraints.table_schema" => $this->_light->quote($this->_pu->getDatabase()), + "information_schema.table_constraints.table_name" => $this->_light->quote($name), + "information_schema.key_column_usage.table_schema" => $this->_light->quote($this->_pu->getDatabase()), + "information_schema.key_column_usage.table_name" => $this->_light->quote($name), + "information_schema.key_column_usage.column_name" => $this->_light->quote($colName), + ) + ) + ->joinArray( + array( + "information_schema.key_column_usage.*", + "information_schema.table_constraints.CONSTRAINT_TYPE", + ), + array( + array( + "side" => "INNER", + "table" => "information_schema.table_constraints", + "cond" => "information_schema.table_constraints.CONSTRAINT_NAME = information_schema.key_column_usage.CONSTRAINT_NAME" + ) + ) + ); + + if (!array_key_exists($propertyName, $this->_properties[$className])) + $this->_properties[$className][$propertyName] = new \Nette\PhpGenerator\Property($propertyName); + + $isForeignKey = count($keys_results) > 0; + $isPrimaryKey = false; + + foreach ($keys_results as $keys_result) { + if (strtolower($keys_result["CONSTRAINT_TYPE"]) === "primary key") { + $this->_properties[$className][$propertyName]->addComment("@id"); + $pKeys[] = array($this->_properties[$className][$propertyName], $colName); + $isForeignKey = false; + $isPrimaryKey = true; + } + + if (strtolower($keys_result["CONSTRAINT_TYPE"]) === "unique") { + $class->addComment("@namedQuery('findBy{$propertyName}', 'SELECT * FROM {$name} WHERE {$name}.{$colName} = :{$propertyName}')"); + $this->_properties[$className][$propertyName]->addComment("@unique"); + $isForeignKey = false; + } + + if (strtolower($keys_result["CONSTRAINT_TYPE"]) === "foreign key") { + $referencedEntity = $this->_generateObjectName($keys_result["REFERENCED_TABLE_NAME"]); + $referencedColumn = $keys_result["REFERENCED_COLUMN_NAME"]; + $referencedProperty = "{$className}Collection"; + + if ($namespace !== null) { + $namespace->addUse($input->getOption(self::OPTION_NAMESPACE) . "\\{$referencedEntity}"); + } + + if ($isPrimaryKey) { + $this->_properties["{$propertyName}Reference"] = new \Nette\PhpGenerator\Property($propertyName); + $this->_properties["{$propertyName}Reference"]->addComment("@oneToMany('{$referencedEntity}', '{$referencedColumn}')"); + $this->_properties["{$propertyName}Reference"]->addComment("@column('{$colName}', '{$colType}')"); + } else { + $this->_properties[$className][$propertyName]->addComment("@oneToMany('{$referencedEntity}', '{$referencedColumn}')"); + } + + if (!array_key_exists($referencedEntity, $this->_properties)) + $this->_properties[$referencedEntity] = array(); + + if (!array_key_exists($referencedProperty, $this->_properties[$referencedEntity])) + $this->_properties[$referencedEntity][$referencedProperty] = new \Nette\PhpGenerator\Property($referencedProperty); + + $this->_properties[$referencedEntity][$referencedProperty]->addComment("@manyToOne('{$className}', '${colName}')"); + $this->_properties[$referencedEntity][$referencedProperty]->addComment("@column('{$referencedColumn}')"); + + $isForeignKey = true; + } + } + + if ($colDefault !== null) { + $this->_properties[$className][$propertyName]->addComment("@column('{$colName}', '{$colType}', '{$colDefault}')"); + } else { + $this->_properties[$className][$propertyName]->addComment("@column('{$colName}', '{$colType}')"); + } + + if (!$isForeignKey) { + if (strtolower($result["EXTRA"]) === "auto_increment") { + $this->_properties[$className][$propertyName]->addComment("@autoIncrement"); + } + + if (strtolower($result["IS_NULLABLE"]) === "no") { + $this->_properties[$className][$propertyName]->addComment("@notNull"); + } + + if ($result["CHARACTER_MAXIMUM_LENGTH"] !== null) { + $this->_properties[$className][$propertyName]->addComment("@size({$result['CHARACTER_MAXIMUM_LENGTH']})"); + } + } + } + + if (count($pKeys) > 1) { + $pkFile = (new \Nette\PhpGenerator\PhpFile) + ->addComment("THIS FILE IS GENERATED BY LIGHTQL" . PHP_EOL) + ->addComment("LightQL entity primary key for the table \"{$name}\"" . PHP_EOL) + ->addComment("Generated at " . date("Y-m-d H:i:s")); + + if ($namespace !== null) { + $pkNamespace = $pkFile->addNamespace($input->getOption(self::OPTION_NAMESPACE)); + $pkNamespace->addUse(IPrimaryKey::class); + $pkClass = $pkNamespace->addClass($pkClassName); + } else { + $pkFile->addUse(IPrimaryKey::class); + $pkClass = $pkFile->addClass($pkClassName); + } + + $pkClass->addImplement(IPrimaryKey::class); + + foreach ($pKeys as $key) { + $pkClass->addMember($key[0]); + } + + $class->addProperty($pkClassName)->addComment("@id"); + } else { + $class->addComment("@namedQuery('findById', 'SELECT * FROM {$name} WHERE {$name}.{$pKeys[0][1]} = :id')"); + } + + foreach ($this->_properties[$className] as $_ => $property) + $class->addMember($property); + + $printer = new EntityFilePrinter; + + $outClass = $printer->printFile($file); + file_put_contents($input->getOption(self::OPTION_OUTPUT_DIR) . DIRECTORY_SEPARATOR . "{$className}.php", $outClass); + $output->writeln(" - Entity {$className} generated from table {$name}"); + + if ($pkClass !== null) { + $outClass = $printer->printFile($pkFile); + file_put_contents($input->getOption(self::OPTION_OUTPUT_DIR) . DIRECTORY_SEPARATOR . "{$pkClassName}.php", $outClass); + $output->writeln(" - Primary Key Class {$pkClassName} generated from table {$name}"); + } + } + + private function _generateObjectName(string $name): string + { + return implode("", array_map(function ($item) { + return ucfirst(trim($item)); + }, preg_split("/[_-]/", $name))); + } +} diff --git a/src/LightQL/CLI/Utils/EntityFilePrinter.php b/src/LightQL/CLI/Utils/EntityFilePrinter.php new file mode 100644 index 0000000..a32782b --- /dev/null +++ b/src/LightQL/CLI/Utils/EntityFilePrinter.php @@ -0,0 +1,69 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\CLI\Utils; + +class EntityFilePrinter extends \Nette\PhpGenerator\Printer +{ + public function printNamespace(\Nette\PhpGenerator\PhpNamespace $namespace): string + { + $name = $namespace->getName(); + + $uses = []; + foreach ($namespace->getUses() as $alias => $original) { + if ($alias === $original || substr($original, - (strlen($alias) + 1)) === '\\' . $alias) { + $uses[$original] = "use {$original};"; + } else { + $uses[$original] = "use {$original} as {$alias};"; + } + } + + $classes = []; + foreach ($namespace->getClasses() as $className => $class) { + $classes[] = $this->printClass($class, $namespace); + unset($uses["{$name}\\{$className}"]); + unset($uses[$className]); + } + + $body = ($uses ? implode("\n", $uses) . "\n\n" : '') + . implode("\n", $classes); + + if ($namespace->hasBracketedSyntax()) { + return 'namespace' . ($name ? " {$name}" : '') . "\n{\n" + . $this->indent($body) + . "}\n"; + } else { + return ($name ? "namespace {$name};\n\n" : '') + . $body; + } + } +} diff --git a/src/LightQL/Entities/EntitiesCollection.php b/src/LightQL/Entities/EntitiesCollection.php new file mode 100644 index 0000000..cbdb473 --- /dev/null +++ b/src/LightQL/Entities/EntitiesCollection.php @@ -0,0 +1,273 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Entities; + +use ArgumentCountError; +use Exception; + +/** + * Entities Collection + * + * A collection of entities which allow ordering, filtering and more. + * + * @category Entities + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/EntitiesCollection + */ +class EntitiesCollection implements \ArrayAccess, \SeekableIterator, \Countable +{ + /** + * The list of entities + * + * @var IEntity[] + */ + private $_entities; + + /** + * The current position in the iterator + * + * @var int + */ + private $_position = 0; + + public function __construct(array $entities) + { + $this->_entities = $entities; + } + + public function order(...$params) : self + { + if (count($params) === 1) { + if (is_callable($params[0])) { + $this->_sort($params[0]); + } elseif (is_string($params[0])) { + $this->_sort(function($current, $next) use ($params) { + return ($current->get($params[0]) > $next->get($params[0])); + }); + } + } elseif (count($params) === 2 && is_string($params[0]) && is_string($params[1])) { + if (strtolower($params[1]) !== 'asc' && strtolower($params[1]) !== 'desc') { + throw new Exception("The second parameter must be 'asc' or 'desc'"); + } + + $this->_sort(function($current, $next) use ($params) { + if (strtolower($params[1]) === 'asc') { + return ($current->get($params[0]) > $next->get($params[0])); + } elseif (strtolower($params[1]) === 'desc') { + return ($current->get($params[0]) < $next->get($params[0])); + } + }); + } else { + throw new ArgumentCountError(); + } + + return $this; + } + + public function filter(callable $func) : self + { + $result = array(); + + foreach ($this->_entities as $entity) { + if ($func($entity) === true) { + array_push($result, $entity); + } + } + + $this->_entities = array_values($result); + + return $this; + } + + public function toArray() + { + return $this->_entities; + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return bool true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists($offset) + { + return isset($this->_entities[$offset]); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->_entities[$offset] : null; + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + $this->_entities[$offset] = $value; + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + unset($this->_entities[$offset]); + } + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 5.0.0 + */ + public function current() + { + $values = array_values($this->_entities); + + return $values[$this->_position]; + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function next() + { + $this->_position++; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 5.0.0 + */ + public function key() + { + $keys = array_keys($this->_entities); + + return $keys[$this->_position]; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return bool The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 5.0.0 + */ + public function valid() + { + return $this->_position < count($this->_entities); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 5.0.0 + */ + public function rewind() + { + $this->_position = 0; + } + + /** + * Seeks to a position + * @link http://php.net/manual/en/seekableiterator.seek.php + * @param int $position

+ * The position to seek to. + *

+ * @return void + * @since 5.1.0 + */ + public function seek($position) + { + $this->_position = $position; + } + + public function count() + { + return count($this->_entities); + } + + private function _sort(callable $func) + { + for ($i = 0, $l = count($this->_entities); $i < $l - 1; $i++) { + for ($j = $i+1; $j < $l; $j++) { + if ($func($this->_entities[$i], $this->_entities[$j])) { + $t = $this->_entities[$i]; + $this->_entities[$i] = $this->_entities[$j]; + $this->_entities[$j] = $t; + } + } + } + } +} diff --git a/src/LightQL/Entities/Entity.php b/src/LightQL/Entities/Entity.php index 94d6ebf..d37a2bc 100644 --- a/src/LightQL/Entities/Entity.php +++ b/src/LightQL/Entities/Entity.php @@ -35,7 +35,6 @@ use ElementaryFramework\Annotations\Annotations; use ElementaryFramework\Annotations\IAnnotation; use ElementaryFramework\LightQL\Exceptions\EntityException; -use ElementaryFramework\LightQL\Exceptions\AnnotationException; /** * Entity @@ -67,6 +66,20 @@ abstract class Entity implements IEntity */ protected $raw = array(); + /** + * The raw data with the value transformer applied. + * + * @var array + */ + private $_polished = array(); + + /** + * The name of the table managed by the current entity. + * + * @var string + */ + private $_table; + /** * The reflection class of this entity. * @@ -87,7 +100,7 @@ abstract class Entity implements IEntity * @param array $data The raw database data. * * @throws EntityException - * @throws AnnotationException + * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException * @throws \ReflectionException */ public function __construct(array $data = array()) @@ -96,8 +109,10 @@ public function __construct(array $data = array()) throw new EntityException("Cannot create an entity without the @entity annotation."); } + $this->_table = Annotations::ofClass($this, "@entity")[0]->table; + $this->_reflection = new \ReflectionClass($this); - $properties = $this->_reflection->getProperties(); + $properties = $this->_reflection->getProperties(T_PUBLIC); $pkFound = false; @@ -140,9 +155,20 @@ public function __construct(array $data = array()) */ public function hydrate(array $data) { + /** @var IValueTransformer $valueTransformer */ + $valueTransformer = Entity::getValueTransformerOfEntity($this); + // Merge values foreach ($data as $name => $value) { + // Save the raw value $this->raw[$name] = $value; + + // Save the polished value + if (is_string($name)) { + $this->_polished[$name] = $valueTransformer !== null + ? $valueTransformer->toEntityValue($this->_table, $name, $value) + : $value; + } } // Populate @column properties @@ -154,7 +180,8 @@ public function hydrate(array $data) } elseif (\is_null($this->{$property}) || $this->{$property} === null) { $this->{$property} = $column->getDefault(); } - } else { + } elseif (array_key_exists($column->getName(), $this->raw)) { + // TODO: Find a way to fill the property with the good value $this->{$property} = null; } } @@ -166,24 +193,22 @@ public function hydrate(array $data) * @param string $column The table column name. * * @return mixed + * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException */ public function get(string $column) { - // Try to get the raw value - if ($this->_exists($column)) { - return $this->raw[$column]; - } - // Try to get the property value /** @var Column $c */ foreach ($this->_columns as $property => $c) { if ($c->getName() === $column && isset($this->{$property})) { + if ($c->isManyToOne || $c->isManyToMany) { + // Find the good property + continue; + } + 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) { + if ($c->isOneToMany) { // Resolve the referenced column $referencedColumn = $this->_getMetadata($property, "@oneToMany", "referencedColumn"); return $this->{$property}->get($referencedColumn); @@ -192,12 +217,17 @@ public function get(string $column) $referencedColumn = $this->_getMetadata($property, "@oneToOne", "referencedColumn"); return $this->{$property}->get($referencedColumn); } - } else { + } elseif (!$c->isManyToOne && $this->{$property} !== null) { return $this->{$property}; } } } + // Try to get the raw value + if ($this->_exists($column)) { + return $this->_polished[$column]; + } + // The value definitively doesn't exist return null; } @@ -224,6 +254,16 @@ public function getColumns(): array return $this->_columns; } + /** + * Gets the entity as array. + * + * @return array + */ + public function getRawEntity(): array + { + return $this->raw; + } + /** * Checks if a property has the given annotation. * @@ -280,4 +320,52 @@ private function _exists(string $column): bool { return array_key_exists($column, $this->raw); } + + /** + * @param $entity + * @return IValueValidator|null + * @throws AnnotationException + * @throws EntityException + * @throws \ReflectionException + */ + public static function getValueValidatorOfEntity($entity) + { + if (Annotations::classHasAnnotation($entity, "@validator")) { + $validatorAnnotation = Annotations::ofClass($entity, "@validator"); + + if (\is_subclass_of($validatorAnnotation[0]->validator, IValueValidator::class)) { + $validatorClass = new \ReflectionClass($validatorAnnotation[0]->validator); + + return $validatorClass->newInstance(); + } else { + throw new EntityException("The value validator of this entity doesn't implement the IValueValidator interface."); + } + } + + return null; + } + + /** + * @param $entity + * @return IValueTransformer|null + * @throws AnnotationException + * @throws EntityException + * @throws \ReflectionException + */ + public static function getValueTransformerOfEntity($entity) + { + if (Annotations::classHasAnnotation($entity, "@transformer")) { + $transformerAnnotation = Annotations::ofClass($entity, "@transformer"); + + if (\is_subclass_of($transformerAnnotation[0]->transformer, IValueTransformer::class)) { + $transformerClass = new \ReflectionClass($transformerAnnotation[0]->transformer); + + return $transformerClass->newInstance(); + } else { + throw new EntityException("The value transformer of this entity doesn't implement the IValueTransformer interface."); + } + } + + return null; + } } diff --git a/src/LightQL/Entities/EntityManager.php b/src/LightQL/Entities/EntityManager.php index 3acd650..8e17f76 100644 --- a/src/LightQL/Entities/EntityManager.php +++ b/src/LightQL/Entities/EntityManager.php @@ -33,7 +33,10 @@ namespace ElementaryFramework\LightQL\Entities; use ElementaryFramework\Annotations\Annotations; +use ElementaryFramework\Annotations\Exceptions\AnnotationException; use ElementaryFramework\LightQL\Exceptions\EntityException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Exceptions\ValueValidatorException; use ElementaryFramework\LightQL\LightQL; use ElementaryFramework\LightQL\Persistence\PersistenceUnit; @@ -42,7 +45,6 @@ * * Manage all entities, using one same persistence unit. * - * @final * @category Entities * @package LightQL * @author Nana Axel @@ -50,6 +52,13 @@ */ final class EntityManager { + /** + * The array of existing LightQL connections. + * + * @var array + */ + private static $_connections = array(); + /** * The persistence unit of this entity * manager. @@ -71,23 +80,30 @@ final class EntityManager * * @param PersistenceUnit $persistenceUnit The persistence unit to use in this manager. * - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws LightQLException */ public function __construct(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() - ) - ); + if (!array_key_exists($persistenceUnit->getKey(), static::$_connections)) { + // 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(), + "port" => $this->_persistenceUnit->getPort() + ) + ); + + static::$_connections[$persistenceUnit->getKey()] = $this->_lightql; + } else { + $this->_lightql = static::$_connections[$persistenceUnit->getKey()]; + } } /** @@ -96,13 +112,13 @@ public function __construct(PersistenceUnit $persistenceUnit) * @param string $entityClass The class name of the entity to find. * @param mixed $id The value of the primary key. * - * @return array Raw data from database. + * @return array|null Raw data from database or null if no data. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException + * @throws AnnotationException + * @throws LightQLException * @throws \ReflectionException */ - public function find(string $entityClass, $id): array + public function find(string $entityClass, $id): ?array { $entityAnnotation = Annotations::ofClass($entityClass, "@entity"); @@ -114,20 +130,20 @@ public function find(string $entityClass, $id): array if ($id instanceof IPrimaryKey) { $pkClass = new \ReflectionClass($id); - $properties = $pkClass->getProperties(); + $properties = $pkClass->getProperties(T_PUBLIC); /** @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()}); + $where[$name] = $this->_lightql->parseValue($id->{$property->getName()}); } } } else { foreach ($columns as $property => $column) { if (count($where) === 0) { if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($id); + $where[$column->getName()] = $this->_lightql->parseValue($id); } } else break; } @@ -144,10 +160,14 @@ public function find(string $entityClass, $id): array /** * Persists an entity into the database. * - * @param Entity $entity The entity to create. + * @param Entity &$entity The entity to create. + * + * @return array * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException + * @throws ValueValidatorException + * @throws AnnotationException + * @throws \ReflectionException */ public function persist(Entity &$entity) { @@ -158,6 +178,8 @@ public function persist(Entity &$entity) $autoIncrementProperty = null; $idProperty = null; + $valueValidator = Entity::getValueValidatorOfEntity($entity); + $valueTransformer = Entity::getValueTransformerOfEntity($entity); /** @var Column $column */ foreach ($columns as $property => $column) { @@ -170,6 +192,19 @@ public function persist(Entity &$entity) } } + if ($idProperty === null) { + $entityReflection = new \ReflectionClass($entity); + $entityProperties = $entityReflection->getProperties(T_PUBLIC); + + /** @var \ReflectionProperty $property */ + foreach ($entityProperties as $property) { + if (Annotations::propertyHasAnnotation($entity, $property->name, "@id")) { + $idProperty = $property->name; + 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 @@ -203,35 +238,95 @@ public function persist(Entity &$entity) } } + /** @var Column $column */ foreach ($columns as $property => $column) { - $fieldAndValues[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + if ($column->isManyToOne) { + continue; + } + + $value = $this->_lightql->parseValue($entity->get($column->getName())); + + if ($valueValidator !== null && !$valueValidator->validate($entity, $property)) { + throw new ValueValidatorException($property); + } + + if ($valueTransformer !== null) { + $value = $this->_lightql->parseValue($valueTransformer->toDatabaseValue($entity, $property)); + } + + $fieldAndValues[$column->getName()] = $value; + } + + if (Annotations::classHasAnnotation($entity, "@pkClass")) { + $pkClassName = Annotations::ofClass($entity, "@pkClass")[0]->name; + $pkClassReflection = new \ReflectionClass($pkClassName); + $pkClassProperties = $pkClassReflection->getProperties(T_PUBLIC); + + /** @var \ReflectionProperty $property */ + foreach ($pkClassProperties as $property) { + if (Annotations::propertyHasAnnotation($pkClassName, $property->name, "@column")) { + $columnAnnotations = Annotations::ofProperty($pkClassName, $property->name, "@column"); + $fieldAndValues[$columnAnnotations[0]->name] = $this->_lightql->parseValue($entity->{$idProperty}->{$property->name}); + } + } + } + + $inTransaction = $this->_lightql->inTransaction(); + + if (!$inTransaction) { + $this->_lightql->beginTransaction(); } - $this->_lightql->beginTransaction(); try { $this->_lightql ->from($entityAnnotation[0]->table) ->insert($fieldAndValues); - if ($autoIncrementProperty !== null) { - $entity->$autoIncrementProperty = $this->_lightql->lastInsertID(); + if (!$inTransaction) { + $this->_lightql->commit(); } - - $this->_lightql->commit(); } catch (\Exception $e) { - $this->_lightql->rollback(); + if (!$inTransaction) { + $this->_lightql->rollback(); + } - throw new EntityException($e->getMessage()); + throw new EntityException("Unable to persist the entity. See internal exception to learn more.", 0, $e); } + + $where = array(); + if ($autoIncrementProperty !== null) { + $where[$columns[$autoIncrementProperty]->getName()] = $this->_lightql->lastInsertID(); + } elseif ($entity->{$idProperty} instanceof IPrimaryKey) { + $pkClassName = Annotations::ofClass($entity, "@pkClass")[0]->name; + $pkClassReflection = new \ReflectionClass($pkClassName); + $pkClassProperties = $pkClassReflection->getProperties(T_PUBLIC); + + /** @var \ReflectionProperty $property */ + foreach ($pkClassProperties as $property) { + if (Annotations::propertyHasAnnotation($pkClassName, $property->name, "@column")) { + $columnAnnotations = Annotations::ofProperty($pkClassName, $property->name, "@column"); + $where[$columnAnnotations[0]->name] = $this->_lightql->parseValue($entity->{$idProperty}->{$property->name}); + } + } + } else { + $where[$columns[$idProperty]->getName()] = $this->_lightql->parseValue($entity->{$idProperty}); + } + + return $this->_lightql + ->from($entityAnnotation[0]->table) + ->where($where) + ->selectFirst(); } /** * Merges the entity in the database with the given one. * - * @param Entity $entity The entity to edit. + * @param Entity &$entity The entity to edit. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException + * @throws ValueValidatorException + * @throws AnnotationException + * @throws \ReflectionException */ public function merge(Entity &$entity) { @@ -239,46 +334,72 @@ public function merge(Entity &$entity) $columns = $entity->getColumns(); $fieldAndValues = array(); + $valueValidator = Entity::getValueValidatorOfEntity($entity); + $valueTransformer = Entity::getValueTransformerOfEntity($entity); $where = array(); $entityReflection = new \ReflectionClass($entity); - $entityProperties = $entityReflection->getProperties(); + $entityProperties = $entityReflection->getProperties(T_PUBLIC); /** @var \ReflectionProperty $property */ foreach ($entityProperties as $property) { $id = $entity->{$property->getName()}; if ($id instanceof IPrimaryKey) { $propertyReflection = new \ReflectionClass($id); - $propertyProperties = $propertyReflection->getProperties(); + $propertyProperties = $propertyReflection->getProperties(T_PUBLIC); foreach ($propertyProperties as $key) { $name = Annotations::ofProperty($id, $key->getName(), "@column")[0]->name; - $where[$name] = $this->_lightql->quote($id->{$key->getName()}); + $where[$name] = $this->_lightql->parseValue($id->{$key->getName()}); } break; } } + /** @var Column $column */ foreach ($columns as $property => $column) { - $fieldAndValues[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + if ($column->isManyToOne) { + continue; + } + + $value = $this->_lightql->parseValue($entity->get($column->getName())); + + if ($valueValidator !== null && !$valueValidator->validate($entity, $property)) { + throw new ValueValidatorException($property); + } + + if ($valueTransformer !== null) { + $value = $this->_lightql->parseValue($valueTransformer->toDatabaseValue($entity, $property)); + } + + $fieldAndValues[$column->getName()] = $value; if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->getName()] = $this->_lightql->parseValue($entity->get($column->getName())); } } - $this->_lightql->beginTransaction(); + $inTransaction = $this->_lightql->inTransaction(); + + if (!$inTransaction) { + $this->_lightql->beginTransaction(); + } + try { $this->_lightql ->from($entityAnnotation[0]->table) ->where($where) ->update($fieldAndValues); - $this->_lightql->commit(); + if (!$inTransaction) { + $this->_lightql->commit(); + } } catch (\Exception $e) { - $this->_lightql->rollback(); + if (!$inTransaction) { + $this->_lightql->rollback(); + } throw new EntityException($e->getMessage()); } @@ -287,34 +408,34 @@ public function merge(Entity &$entity) /** * Removes an entity from the database. * - * @param Entity $entity The entity to delete. + * @param Entity &$entity The entity to delete. * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException + * @throws EntityException + * @throws AnnotationException + * @throws \ReflectionException */ public function delete(Entity &$entity) { $entityAnnotation = Annotations::ofClass($entity, "@entity"); $columns = $entity->getColumns(); - $fieldAndValues = array(); $where = array(); $pk = array(); $entityReflection = new \ReflectionClass($entity); - $entityProperties = $entityReflection->getProperties(); + $entityProperties = $entityReflection->getProperties(T_PUBLIC); /** @var \ReflectionProperty $property */ foreach ($entityProperties as $property) { $id = $entity->{$property->getName()}; if ($id instanceof IPrimaryKey) { $propertyReflection = new \ReflectionClass($id); - $propertyProperties = $propertyReflection->getProperties(); + $propertyProperties = $propertyReflection->getProperties(T_PUBLIC); foreach ($propertyProperties as $key) { $name = Annotations::ofProperty($id, $key->getName(), "@column")[0]->name; - $where[$name] = $this->_lightql->quote($id->{$key->getName()}); + $where[$name] = $this->_lightql->parseValue($id->{$key->getName()}); $pk[] = $property->getName(); } @@ -324,12 +445,17 @@ public function delete(Entity &$entity) foreach ($columns as $property => $column) { if ($column->isPrimaryKey) { - $where[$column->getName()] = $this->_lightql->quote($entity->get($column->getName())); + $where[$column->getName()] = $this->_lightql->parseValue($entity->get($column->getName())); $pk[] = $property; } } - $this->_lightql->beginTransaction(); + $inTransaction = $this->_lightql->inTransaction(); + + if (!$inTransaction) { + $this->_lightql->beginTransaction(); + } + try { $this->_lightql ->from($entityAnnotation[0]->table) @@ -338,13 +464,17 @@ public function delete(Entity &$entity) if (count($pk) > 0) { foreach ($pk as $item) { - $entity->$item = null; + $entity->{$item} = null; } } - $this->_lightql->commit(); + if (!$inTransaction) { + $this->_lightql->commit(); + } } catch (\Exception $e) { - $this->_lightql->rollback(); + if (!$inTransaction) { + $this->_lightql->rollback(); + } throw new EntityException($e->getMessage()); } diff --git a/src/LightQL/Entities/GenericEntity.php b/src/LightQL/Entities/GenericEntity.php new file mode 100644 index 0000000..825b252 --- /dev/null +++ b/src/LightQL/Entities/GenericEntity.php @@ -0,0 +1,165 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Entities; + +/** + * Generic Entity + * + * Represent an entity which can adapt to any table structure. + * + * @package ElementaryFramework\LightQL\Entities + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/GenericEntity + */ +final class GenericEntity implements IEntity +{ + /** + * The name of the table held by this entity. + * + * @var string + */ + private $_name; + + /** + * The name of the column defined as the primary key of the table, if any. + * + * @var string + */ + private $_pk; + + /** + * The collection of key-value pairs, in which keys represent columns names. + * + * @var array + */ + private $_data; + + /** + * GenericEntity constructor. + * + * @param string $name The name of the table managed by this GenericEntity. + * @param null|string $pk The name of the column defined as the primary key of the table, if any. + * @param array $data The initial values of columns in this GenericEntity. + */ + public function __construct(string $name, string $pk, array $data = array()) + { + $this->_name = $name; + $this->_pk = $pk; + + $this->hydrate($data); + } + + /** + * Populates data in the entity. + * + * @param array $data The raw database data. + */ + public function hydrate(array $data) + { + foreach ($data as $column => $value) { + $this->set($column, $value); + } + } + + /** + * Sets the raw value of a table column. + * + * @param string $column The table column name. + * @param mixed $value The table column value. + */ + public function set(string $column, $value) + { + $this->_data[$column] = $value; + } + + /** + * @param string $name The column's name. + * + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * @param string $name The column's name. + * @param mixed $value The value to assign. + */ + public function __set($name, $value) + { + $this->set($name, $value); + } + + /** + * Gets the raw value of a table column. + * + * @param string $column The table column name. + * + * @return mixed + */ + public function get(string $column) + { + return array_key_exists($column, $this->_data) ? $this->_data[$column] : null; + } + + /** + * Returns the name of the table managed by this GenericEntity. + * + * @return string + */ + public function getName(): string + { + return $this->_name; + } + + /** + * Return the data. + * + * @return array + */ + public function getData(): array + { + return $this->_data; + } + + /** + * Return the column name of the primary key. + * + * @return string + */ + public function getPk(): string + { + return $this->_pk; + } +} \ No newline at end of file diff --git a/src/LightQL/Entities/IValueTransformer.php b/src/LightQL/Entities/IValueTransformer.php new file mode 100644 index 0000000..595edf8 --- /dev/null +++ b/src/LightQL/Entities/IValueTransformer.php @@ -0,0 +1,67 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Entities; + +/** + * IValueTransformer + * + * Defines a class as the value transformer of an entity. + * + * @category Entities + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/IValueTransformer + */ +interface IValueTransformer +{ + /** + * Transforms a value before send it to the database. + * + * @param Entity $entity The entity which will be sent to the database. + * @param string $property The name of the property to transform value. + * + * @return mixed The transformed value. + */ + function toDatabaseValue(Entity $entity, string $property); + + /** + * Transforms a value before send it to the entity. + * + * @param string $table The name of the table which provide the value. + * @param string $column The name of the column which hold the value. + * @param mixed $value The value to transform. + * + * @return mixed The transformed value. + */ + function toEntityValue(string $table, string $column, $value); +} diff --git a/src/LightQL/Entities/IValueValidator.php b/src/LightQL/Entities/IValueValidator.php new file mode 100644 index 0000000..079413e --- /dev/null +++ b/src/LightQL/Entities/IValueValidator.php @@ -0,0 +1,56 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Entities; + +/** + * IValueValidator + * + * Defines a class as the value validator of an entity. + * + * @category Entities + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Entities/IValueValidator + */ +interface IValueValidator +{ + /** + * Validates a value before send it to the database. + * + * @param Entity $entity The entity which will be sent to the database. + * @param string $property The name of the property to validate. + * + * @return bool true if the value passes the validator, false otherwise. + */ + function validate(Entity $entity, string $property): bool; +} diff --git a/src/LightQL/Entities/Query.php b/src/LightQL/Entities/Query.php index e718042..19e7789 100644 --- a/src/LightQL/Entities/Query.php +++ b/src/LightQL/Entities/Query.php @@ -1,174 +1,166 @@ - - * @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; - } -} + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Entities; + +use ElementaryFramework\Annotations\Annotations; +use ElementaryFramework\LightQL\Exceptions\QueryException; +use ElementaryFramework\LightQL\Sessions\Facade; + +/** + * 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 facade running this query. + * + * @var Facade + */ + private $_facade; + + /** + * 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 Facade $facade + */ + public function __construct(Facade $facade) + { + $this->_facade = $facade; + } + + /** + * 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->_facade + ->getEntityManager() + ->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 EntitiesCollection + */ + public function getResults(): EntitiesCollection + { + if ($this->_query === null) { + throw new QueryException("Cannot get results, have you ran the query?"); + } + + $className = $this->_facade->getEntityClassName(); + + $results = $this->_facade->_parseRawEntities( + $this->_query->fetchAll(), + $className, + Annotations::ofClass($className, "@entity") + ); + + return new EntitiesCollection($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/Exceptions/EntityException.php b/src/LightQL/Exceptions/EntityException.php index 6e6863c..0f9ad94 100644 --- a/src/LightQL/Exceptions/EntityException.php +++ b/src/LightQL/Exceptions/EntityException.php @@ -32,6 +32,8 @@ namespace ElementaryFramework\LightQL\Exceptions; +use Throwable; + /** * Entity Exception * @@ -42,4 +44,8 @@ */ class EntityException extends \Exception { + public function __construct($message = "", $code = 0, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } } diff --git a/src/LightQL/Exceptions/OperationCancelledException.php b/src/LightQL/Exceptions/OperationCancelledException.php new file mode 100644 index 0000000..c47d3a3 --- /dev/null +++ b/src/LightQL/Exceptions/OperationCancelledException.php @@ -0,0 +1,49 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Exceptions; + +/** + * Operation Cancelled Exception + * + * @category Exceptions + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/OperationCancelledException + */ +class OperationCancelledException extends \Exception +{ + public function __construct(string $listenerClassName) + { + parent::__construct("The operation has been cancelled by the listener: \"{$listenerClassName}\""); + } +} diff --git a/src/LightQL/Exceptions/ValueValidatorException.php b/src/LightQL/Exceptions/ValueValidatorException.php new file mode 100644 index 0000000..146d586 --- /dev/null +++ b/src/LightQL/Exceptions/ValueValidatorException.php @@ -0,0 +1,54 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Exceptions; + +/** + * Entity Exception + * + * @category Exceptions + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Exceptions/ValueValidatorException + */ +class ValueValidatorException extends \Exception +{ + /** + * Thrown when a value can't pass the validator of an entity. + * + * @param string $property The name of the property. + */ + public function __construct(string $property) + { + parent::__construct("The value of the property \"{$property}\" have not pass the validator."); + } +} diff --git a/src/LightQL/LightQL.php b/src/LightQL/LightQL.php index 5f8b9fd..b192ef3 100644 --- a/src/LightQL/LightQL.php +++ b/src/LightQL/LightQL.php @@ -38,6 +38,7 @@ use ElementaryFramework\LightQL\Annotations\EntityAnnotation; use ElementaryFramework\LightQL\Annotations\IdAnnotation; use ElementaryFramework\LightQL\Annotations\IdGeneratorAnnotation; +use ElementaryFramework\LightQL\Annotations\ListenerAnnotation; use ElementaryFramework\LightQL\Annotations\ManyToManyAnnotation; use ElementaryFramework\LightQL\Annotations\ManyToOneAnnotation; use ElementaryFramework\LightQL\Annotations\NamedQueryAnnotation; @@ -45,8 +46,11 @@ use ElementaryFramework\LightQL\Annotations\OneToManyAnnotation; use ElementaryFramework\LightQL\Annotations\OneToOneAnnotation; use ElementaryFramework\LightQL\Annotations\PersistenceUnitAnnotation; +use ElementaryFramework\LightQL\Annotations\PkClassAnnotation; use ElementaryFramework\LightQL\Annotations\SizeAnnotation; +use ElementaryFramework\LightQL\Annotations\TransformerAnnotation; use ElementaryFramework\LightQL\Annotations\UniqueAnnotation; +use ElementaryFramework\LightQL\Annotations\ValidatorAnnotation; use ElementaryFramework\LightQL\Exceptions\LightQLException; /** @@ -78,6 +82,7 @@ public static function registerAnnotations() $manager->registerAnnotation("entity", EntityAnnotation::class); $manager->registerAnnotation("id", IdAnnotation::class); $manager->registerAnnotation("idGenerator", IdGeneratorAnnotation::class); + $manager->registerAnnotation("listener", ListenerAnnotation::class); $manager->registerAnnotation("manyToMany", ManyToManyAnnotation::class); $manager->registerAnnotation("manyToOne", ManyToOneAnnotation::class); $manager->registerAnnotation("namedQuery", NamedQueryAnnotation::class); @@ -85,8 +90,11 @@ public static function registerAnnotations() $manager->registerAnnotation("oneToMany", OneToManyAnnotation::class); $manager->registerAnnotation("oneToOne", OneToOneAnnotation::class); $manager->registerAnnotation("persistenceUnit", PersistenceUnitAnnotation::class); + $manager->registerAnnotation("pkClass", PkClassAnnotation::class); $manager->registerAnnotation("size", SizeAnnotation::class); + $manager->registerAnnotation("transformer", TransformerAnnotation::class); $manager->registerAnnotation("unique", UniqueAnnotation::class); + $manager->registerAnnotation("validator", ValidatorAnnotation::class); } /** @@ -255,7 +263,7 @@ public function __construct(array $options = null) return false; } } else { - if (isset($options["port"]) && is_int($options["port"] * 1)) { + if (isset($options["port"]) && is_int($options["port"] * 1) && intval($options["port"]) > -1) { $port = $options["port"]; } @@ -352,7 +360,7 @@ public function __construct(array $options = null) $stack[] = is_int($key) ? $value : "{$key}={$value}"; } - $this->_dsn = $this->_driver . ":" . implode($stack, ";"); + $this->_dsn = $this->_driver . ":" . implode(";", $stack); if (in_array($this->_dbms, ['mariadb', 'mysql', 'pgsql', 'sybase', 'mssql']) && isset($options['charset'])) { $commands[] = "SET NAMES '{$options['charset']}'"; @@ -445,7 +453,7 @@ public function where($condition): LightQL if (is_int($column)) { $this->_where .= $value; } else { - $parts = explode(" ", $this->parseValue($value)); + $parts = explode(" ", $value); foreach (self::$_operators as $operator) { if (in_array($operator, $parts, true) && $parts[0] === $operator) { $operand = $operator; @@ -488,7 +496,7 @@ public function order(string $column, string $mode = "ASC"): LightQL */ public function limit(int $offset, int $count): LightQL { - $this->_limit = " LIMIT {$offset}, {$count} "; + $this->_limit = " LIMIT {$count} OFFSET {$offset}"; return $this; } @@ -614,7 +622,7 @@ protected function resetClauses() * * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException * - * @return array + * @return array|null */ public function selectFirst($columns = "*") { @@ -799,13 +807,13 @@ public function count($columns = "*") $getFieldsData = $this->prepare($this->_queryString); if ($getFieldsData->execute() !== false) { + $this->resetClauses(); + if (null === $this->_group) { - $this->resetClauses(); $data = $getFieldsData->fetch(); return (int) $data['lightql_count']; } - $this->resetClauses(); $res = array(); while ($data = $getFieldsData->fetch()) { @@ -834,13 +842,13 @@ public function insert(array $fieldsAndValues): bool foreach ($fieldsAndValues as $column => $value) { $columns[] = $column; - $values[] = $this->parseValue($value); + $values[] = $value; } $column = implode(",", $columns); $value = implode(",", $values); - $this->_queryString = trim("INSERT INTO {$this->table}({$column}) VALUE ({$value})"); + $this->_queryString = trim("INSERT INTO {$this->table}({$column}) VALUES ({$value})"); $getFieldsData = $this->prepare($this->_queryString); @@ -901,7 +909,7 @@ public function update(array $fieldsAndValues): bool if (is_array($fieldsAndValues)) { foreach ($fieldsAndValues as $column => $value) { $count--; - $updates .= "{$column} = " . $this->parseValue($value); + $updates .= "{$column} = " . $value; $updates .= ($count != 0) ? ", " : ""; } } else { @@ -1004,6 +1012,18 @@ public function quote($value): string return $this->_pdo->quote($value); } + /** + * Checks if we are currently inside a transaction. + * + * @uses \PDO::inTransaction() + * + * @return bool + */ + public function inTransaction(): bool + { + return $this->_pdo->inTransaction(); + } + /** * Disable auto commit mode and start a transaction. * @@ -1054,7 +1074,7 @@ public function parseValue($value): string } elseif (is_bool($value)) { return $value ? "1" : "0"; } else { - return strval($value); + return $this->quote(strval($value)); } } } diff --git a/src/LightQL/Persistence/PersistenceUnit.php b/src/LightQL/Persistence/PersistenceUnit.php index 383063e..ebe68c2 100644 --- a/src/LightQL/Persistence/PersistenceUnit.php +++ b/src/LightQL/Persistence/PersistenceUnit.php @@ -46,6 +46,13 @@ */ class PersistenceUnit { + /** + * The key used to create this persistence unit. + * + * @var string + */ + private $_key; + /** * The DBMS. * @@ -81,6 +88,13 @@ class PersistenceUnit */ private $_password; + /** + * The port number of the DBMS. + * + * @var int + */ + private $_port = -1; + /** * The list of registered persistence unit files. * @@ -123,15 +137,46 @@ public static function purge() private function __construct(string $key) { if (array_key_exists($key, self::$_registry)) { - $filename = basename(self::$_registry[$key]); + $this->_key = $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; if ($extension === "ini") { - $content = parse_ini_file(self::$_registry[$key]); + $content = parse_ini_file($filepath); } elseif ($extension === "json") { - $content = json_decode(file_get_contents(self::$_registry[$key]), true); + $content = json_decode(file_get_contents($filepath), true); + } elseif ($extension === "xml") { + $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."); + } else { + /** @var \DOMElement $node */ + foreach ($dom->documentElement->childNodes as $node) { + switch (strtolower($node->nodeName)) { + case "#text": + break; + case "dbms": $content["DBMS"] = $node->nodeValue; break; + case "hostname": $content["Hostname"] = $node->nodeValue; break; + case "database": $content["DatabaseName"] = $node->nodeValue; break; + case "username": $content["Username"] = $node->nodeValue; break; + case "password": $content["Password"] = $node->nodeValue; break; + case "port": + $content["Port"] = $node->nodeValue; + break; + default: throw new PersistenceUnitException("Invalid persistence unit XML configuration file provided. Unknown configuration item \"{$node->nodeName}\""); + } + } + } } else { throw new PersistenceUnitException("Unsupported file type used to create persistence unit {$filename}."); } @@ -165,8 +210,12 @@ private function __construct(string $key) } else { throw new PersistenceUnitException("Malformed persistence unit configuration file, missing the Password value."); } + + if (array_key_exists("Port", $content)) { + $this->_port = intval($content["Port"]); + } } else { - throw new PersistenceUnitException('Unable to find the persistence unit with the key "' . $key . '". Have you registered this persistence unit?'); + throw new PersistenceUnitException("Unable to find the persistence unit with the key \"{$key}\". Have you registered this persistence unit?"); } } @@ -176,8 +225,9 @@ private function __construct(string $key) * @param string $key The persistence unit name. * * @return PersistenceUnit + * @throws PersistenceUnitException */ - public static function create(string $key) + public static function create(string $key): PersistenceUnit { if (array_key_exists($key, self::$_units)) { return self::$_units[$key]; @@ -186,12 +236,22 @@ public static function create(string $key) } } + /** + * Returns the persistence unit key. + * + * @return string + */ + public function getKey(): string + { + return $this->_key; + } + /** * Returns the DBMS. * * @return string */ - public function getDbms() + public function getDbms(): string { return $this->_dbms; } @@ -235,4 +295,14 @@ public function getUsername(): string { return $this->_username; } + + /** + * Returns the port number. + * + * @return int + */ + public function getPort(): int + { + return $this->_port; + } } diff --git a/src/LightQL/Sessions/Facade.php b/src/LightQL/Sessions/Facade.php index d607a68..3907bf0 100644 --- a/src/LightQL/Sessions/Facade.php +++ b/src/LightQL/Sessions/Facade.php @@ -1,576 +1,684 @@ - - * @copyright 2018 Aliens Group, Inc. - * @license MIT - * @version 1.0.0 - * @link http://lightql.na2axl.tk - */ - -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\Entities\EntityManager; -use ElementaryFramework\LightQL\Entities\IEntity; -use ElementaryFramework\LightQL\Entities\Query; -use ElementaryFramework\LightQL\Exceptions\EntityException; -use ElementaryFramework\LightQL\Exceptions\FacadeException; -use ElementaryFramework\LightQL\Persistence\PersistenceUnit; - -/** - * Facade - * - * Base class for all entity facades. - * - * @abstract - * @category Sessions - * @package LightQL - * @author Nana Axel - * @link http://lightql.na2axl.tk/docs/api/LightQL/Sessions/Facade - */ -abstract class Facade implements IFacade -{ - /** - * The entity manager of this facade. - * - * @var EntityManager - */ - protected $entityManager; - - /** - * The entity class name managed by this facade. - * - * @var \ReflectionClass - */ - private $_class; - - /** - * Facade constructor. - * - * @param string $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 inherit from the Entity class. - * @throws AnnotationException When the Facade is unable to read an annotation. - */ - public function __construct($class) - { - if (!Annotations::propertyHasAnnotation($this, "entityManager", "@persistenceUnit")) { - throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no @persistenceUnit annotation."); - } - - if (!Annotations::classHasAnnotation($class, "@entity")) { - throw new EntityException("Cannot create an entity without the @entity annotation."); - } - - if (!is_subclass_of($class, Entity::class)) { - throw new FacadeException("Unable to create a facade. The entity class or object seems to be invalid."); - } - - $this->_class = new \ReflectionClass($class); - - $annotations = Annotations::ofProperty($this, "entityManager", "@persistenceUnit"); - $this->entityManager = new EntityManager(PersistenceUnit::create($annotations[0]->name)); - } - - /** - * Creates an entity. - * - * @param Entity $entity The entity to create. - * - * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\EntityException - */ - public function create(Entity &$entity) - { - 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); - } - } - } 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 - */ - public function edit(Entity &$entity) - { - if (!$this->_class->isInstance($entity)) { - throw new FacadeException("Cannot edit entity. The type of the entity is not valid for this facade."); - } - - $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 - */ - public function delete(Entity &$entity) - { - if (!$this->_class->isInstance($entity)) { - throw new FacadeException("Cannot edit entity. The type of the entity is not valid for this facade."); - } - - $this->entityManager->merge($entity); - $this->entityManager->delete($entity); - } - - /** - * Find an entity. - * - * @param mixed $id The id of the entity to find - * - * @return Entity - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - public function find($id): Entity - { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - - return $this->_parseRawEntity( - $this->entityManager->find($this->getEntityClassName(), $id), - $annotations - ); - } - - /** - * Find all entities. - * - * @return Entity[] - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - public function findAll(): array - { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - - $rawEntities = $this->entityManager - ->getLightQL() - ->from($annotations[0]->table) - ->selectArray(); - - return $this->_parseRawEntities($rawEntities, $annotations); - } - - /** - * Find all entities in the given range. - * - * @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 - */ - public function findRange(int $start, int $length): array - { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - - $rawEntities = $this->entityManager - ->getLightQL() - ->from($annotations[0]->table) - ->limit($start, $length) - ->selectArray(); - - return $this->_parseRawEntities($rawEntities, $annotations); - } - - /** - * Count the number of entities. - * - * @return int - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - public function count(): int - { - $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); - - return $this->entityManager - ->getLightQL() - ->from($annotations[0]->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 - * - * @throws FacadeException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - */ - public function getNamedQuery(string $name): Query - { - if (!Annotations::classHasAnnotation($this->_class->name, "@namedQuery")) { - throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation."); - } - - $namedQueries = Annotations::ofClass($this->getEntityClassName(), "@namedQuery"); - $query = null; - - /** @var NamedQueryAnnotation $namedQuery */ - foreach ($namedQueries as $namedQuery) { - if ($namedQuery->name === $name) { - $query = $namedQuery->query; - break; - } - } - - if ($query === null) { - throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation with the name {$name}."); - } - - $q = new Query($this->entityManager); - $q->setEntity($this->_class); - $q->setQuery($query); - - return $q; - } - - /** - * Fetch data for a many-to-many relation. - * - * @param IEntity $entity The managed entity. - * @param string $property The property in many-to-many relation. - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _fetchManyToMany(&$entity, $property) - { - $manyToMany = Annotations::ofProperty($entity, $property, "@manyToMany"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $entityAnnotations = Annotations::ofClass($entity, "@entity"); - - $mappedPropertyName = null; - - $referencedEntity = new $manyToMany[0]->entity; - foreach ($referencedEntity->getColumns() as $p => $c) { - if ($c->isManyToMany) { - $mappedManyToMany = Annotations::ofProperty($referencedEntity, $p, "@manyToMany"); - if ($mappedManyToMany[0]->crossTable === $manyToMany[0]->crossTable) { - $mappedPropertyName = $p; - break; - } - } - } - unset($referencedEntity); - - if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with a @manyToMany annotation in the entity \"$manyToMany[0]->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}" - ) - ) - ); - - $className = $manyToMany[0]->entity; - $entity->{$property} = array_map(function($item) use ($manyToMany, $className) { - return new $className($item); - }, $results); - } - - /** - * Fetch data for a one-to-many relation. - * - * @param IEntity $entity The managed entity. - * @param string $property The property in one-to-many relation. - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _fetchOneToMany(&$entity, $property) - { - $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}.*"); - - $className = $oneToMany[0]->entity; - - $entity->{$property} = $result; - - if ($result !== null) { - $entity->{$property} = new $className($result); - } - } - - /** - * Fetch data for a many-to-one relation. - * - * @param IEntity $entity The managed entity. - * @param string $property The property in many-to-one relation. - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _fetchManyToOne(&$entity, $property) - { - $manyToOne = Annotations::ofProperty($entity, $property, "@manyToOne"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($manyToOne[0]->entity, "@entity"); - - $mappedPropertyName = $this->_resolveMappedPropertyName($manyToOne[0]->entity, "OneToMany", $manyToOne[0]->referencedColumn); - - if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with @oneToMany annotation in the entity \"{$manyToOne[0]->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); - } - - /** - * Fetch data for a one-to-one relation. - * - * @param IEntity $entity The managed entity. - * @param string $property The property in one-to-one relation. - * - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _fetchOneToOne(&$entity, $property) - { - $oneToOne = Annotations::ofProperty($entity, $property, "@oneToOne"); - $column = Annotations::ofProperty($entity, $property, "@column"); - $referencedEntityAnnotations = Annotations::ofClass($oneToOne[0]->entity, "@entity"); - - $mappedPropertyName = $this->_resolveMappedPropertyName($oneToOne[0]->entity, "OneToOne", $oneToOne[0]->referencedColumn); - - if ($mappedPropertyName === null) { - throw new EntityException("Unable to find a suitable property with @oneToOne annotation in the entity \"{$oneToOne[0]->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; - - $entity->{$property} = $result; - - if ($result !== null) { - $entity->{$property} = new $className($result); - $entity->{$property}->{$mappedPropertyName} = &$entity; - } - } - - /** - * 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. - * - * @return string|null - */ - private function _resolveMappedPropertyName(string $entityClass, string $check, string $column): string - { - $mappedPropertyName = null; - - $referencedEntity = new $entityClass; - foreach ($referencedEntity->getColumns() as $p => $c) { - if ($c->{"is{$check}"} && $c->getName() === $column) { - $mappedPropertyName = $p; - break; - } - } - unset($referencedEntity); - - return $mappedPropertyName; - } - - /** - * 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. - * - * @return Entity[] - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _parseRawEntities($rawEntities, $annotations): array - { - $entities = array(); - - foreach ($rawEntities as $rawEntity) { - array_push($entities, $this->_parseRawEntity($rawEntity, $annotations)); - } - - return $entities; - } - - /** - * Parses raw data to Entity. - * - * @param array $rawEntity Raw entity data provided from database. - * @param array $annotations The set of entity annotations. - * - * @return Entity - * - * @throws EntityException - * @throws \ElementaryFramework\Annotations\Exceptions\AnnotationException - * @throws \ElementaryFramework\LightQL\Exceptions\LightQLException - */ - private function _parseRawEntity($rawEntity, $annotations): Entity - { - /** @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); - } - } - } - - return $entity; - } -} + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Sessions; + +use ElementaryFramework\Annotations\Annotations; +use ElementaryFramework\Annotations\Exceptions\AnnotationException; +use ElementaryFramework\LightQL\Annotations\EntityAnnotation; +use ElementaryFramework\LightQL\Annotations\NamedQueryAnnotation; +use ElementaryFramework\LightQL\Entities\EntitiesCollection; +use ElementaryFramework\LightQL\Entities\Entity; +use ElementaryFramework\LightQL\Entities\EntityManager; +use ElementaryFramework\LightQL\Entities\IEntity; +use ElementaryFramework\LightQL\Entities\IPrimaryKey; +use ElementaryFramework\LightQL\Entities\IValueTransformer; +use ElementaryFramework\LightQL\Entities\Query; +use ElementaryFramework\LightQL\Exceptions\EntityException; +use ElementaryFramework\LightQL\Exceptions\FacadeException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\Exceptions\OperationCancelledException; +use ElementaryFramework\LightQL\Exceptions\PersistenceUnitException; +use ElementaryFramework\LightQL\Exceptions\ValueValidatorException; +use ElementaryFramework\LightQL\Persistence\PersistenceUnit; + +/** + * Facade + * + * Base class for all entity facades. + * + * @abstract + * @category Sessions + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Sessions/Facade + */ +abstract class Facade implements IFacade +{ + /** + * The entity manager of this facade. + * + * @var EntityManager + */ + protected $entityManager; + + /** + * The entity class name managed by this facade. + * + * @var \ReflectionClass + */ + private $_class; + + /** + * The listener of this facade. + * + * @var IFacadeListener + */ + private $_listener; + + /** + * Facade constructor. + * + * @param string $class The entity class name managed by this facade. + * + * @throws AnnotationException When the Facade is unable to read an annotation. + * @throws EntityException When the entity class or object doesn't have an @entity annotation. + * @throws FacadeException When the entity class or object doesn't inherit from the Entity class. + * @throws LightQLException + * @throws PersistenceUnitException + * @throws \ReflectionException + */ + public function __construct($class) + { + if (!Annotations::propertyHasAnnotation($this, "entityManager", "@persistenceUnit")) { + throw new FacadeException("Cannot create the entity facade. The property \"entityManager\" has no @persistenceUnit annotation."); + } + + if (!Annotations::classHasAnnotation($class, "@entity")) { + throw new EntityException("Cannot create an entity without the @entity annotation."); + } + + if (!is_subclass_of($class, Entity::class)) { + throw new FacadeException("Unable to create a facade. The entity class or object need to inherit directly from the class Entity."); + } + + if (Annotations::classHasAnnotation($this, "@listener")) { + $listenerReflection = new \ReflectionClass(Annotations::ofClass($this, "@listener")[0]->listener); + $this->_listener = $listenerReflection->newInstance(); + } + + $this->_class = new \ReflectionClass($class); + + $annotations = Annotations::ofProperty($this, "entityManager", "@persistenceUnit"); + $this->entityManager = new EntityManager(PersistenceUnit::create($annotations[0]->name)); + } + + /** + * Creates an entity. + * + * @param IEntity $entity The entity to create. + * + * @throws AnnotationException + * @throws FacadeException When the facade is unable to create the entity. + * @throws OperationCancelledException When the operation has been cancelled by a listener + * @throws EntityException When the $entity object is not an instance of Entity class + */ + public function create(IEntity &$entity) + { + if (!($entity instanceof Entity)) { + throw new EntityException("The Facade class works only with classes extending Entity."); + } + + if (!$this->_class->isInstance($entity)) { + throw new FacadeException("Cannot create entity. The type of the entity is not valid for this facade."); + } + + if ($this->_listener instanceof IFacadeListener && !$this->_listener->beforeCreate($entity)) { + throw new OperationCancelledException(Annotations::ofClass($this, "@listener")[0]->listener); + } + + try { + $entity = $this->_parseRawEntity( + $this->entityManager->persist($entity), + $this->getEntityClassName(), + Annotations::ofClass($this->getEntityClassName(), "@entity") + ); + + $this->_listener instanceof IFacadeListener && $this->_listener->onCreate($entity); + } catch (\Exception $e) { + throw new FacadeException("Unable to create the entity. See internal exception for more details.", 0, $e); + } + } + + /** + * Edit an entity. + * + * @param IEntity $entity The entity to edit. + * + * @throws AnnotationException + * @throws EntityException + * @throws FacadeException When the facade is unable to edit the entity + * @throws OperationCancelledException When the operation has been cancelled by a listener + * @throws ValueValidatorException + * @throws \ReflectionException + */ + public function edit(IEntity &$entity) + { + if (!($entity instanceof Entity)) { + throw new EntityException("The Facade class works only with classes extending Entity."); + } + + if (!$this->_class->isInstance($entity)) { + throw new FacadeException("Cannot edit entity. The type of the entity is not valid for this facade."); + } + + if ($this->_listener instanceof IFacadeListener && !$this->_listener->beforeEdit($entity)) { + throw new OperationCancelledException(Annotations::ofClass($this, "@listener")[0]->listener); + } + + $this->entityManager->merge($entity); + + $this->_listener instanceof IFacadeListener && $this->_listener->onEdit($entity); + } + + /** + * Delete an entity. + * + * @param IEntity $entity The entity to delete. + * + * @throws AnnotationException + * @throws EntityException + * @throws FacadeException When the facade is unable to delete the entity + * @throws OperationCancelledException When the operation has been cancelled by a listener + * @throws ValueValidatorException + * @throws \ReflectionException + */ + public function delete(IEntity &$entity) + { + if (!($entity instanceof Entity)) { + throw new EntityException("The Facade class works only with classes extending Entity."); + } + + if (!$this->_class->isInstance($entity)) { + throw new FacadeException("Cannot delete entity. The type of the entity is not valid for this facade."); + } + + if ($this->_listener instanceof IFacadeListener && !$this->_listener->beforeDelete($entity)) { + throw new OperationCancelledException(Annotations::ofClass($this, "@listener")[0]->listener); + } + + $this->entityManager->merge($entity); + $this->entityManager->delete($entity); + + $this->_listener instanceof IFacadeListener && $this->_listener->onDelete($entity); + } + + /** + * Find an entity. + * + * @param mixed $id The id of the entity to find + * + * @return IEntity|null + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + * @throws \ReflectionException + */ + public function find($id): ?IEntity + { + return $this->_parseRawEntity( + $this->entityManager->find($this->getEntityClassName(), $id), + $this->getEntityClassName(), + Annotations::ofClass($this->getEntityClassName(), "@entity") + ); + } + + /** + * Find all entities. + * + * @return Entity[] + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + * @throws \ReflectionException + */ + public function findAll(): EntitiesCollection + { + $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); + + $rawEntities = $this->entityManager + ->getLightQL() + ->from($annotations[0]->table) + ->selectArray(); + + return new EntitiesCollection( + $this->_parseRawEntities( + $rawEntities, + $this->getEntityClassName(), + $annotations + ) + ); + } + + /** + * Find all entities in the given range. + * + * @param int $start The starting offset. + * @param int $length The number of entities to find. + * + * @return Entity[] + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + * @throws \ReflectionException + */ + public function findRange(int $start, int $length): EntitiesCollection + { + $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); + + $rawEntities = $this->entityManager + ->getLightQL() + ->from($annotations[0]->table) + ->limit($start, $length) + ->selectArray(); + + return new EntitiesCollection( + $this->_parseRawEntities( + $rawEntities, + $this->getEntityClassName(), + $annotations + ) + ); + } + + /** + * Count the number of entities. + * + * @return int + * + * @throws AnnotationException + * @throws LightQLException + */ + public function count(): int + { + $annotations = Annotations::ofClass($this->getEntityClassName(), "@entity"); + + return $this->entityManager + ->getLightQL() + ->from($annotations[0]->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 + * + * @throws FacadeException + * @throws AnnotationException + */ + public function getNamedQuery(string $name): Query + { + if (!Annotations::classHasAnnotation($this->_class->name, "@namedQuery")) { + throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation."); + } + + $namedQueries = Annotations::ofClass($this->getEntityClassName(), "@namedQuery"); + $query = null; + + /** @var NamedQueryAnnotation $namedQuery */ + foreach ($namedQueries as $namedQuery) { + if ($namedQuery->name === $name) { + $query = $namedQuery->query; + break; + } + } + + if ($query === null) { + throw new FacadeException("The {$this->_class->name} has no @namedQuery annotation with the name {$name}."); + } + + $q = new Query($this); + $q->setQuery($query); + + return $q; + } + + /** + * Fetch data for a many-to-many relation. + * + * @param IEntity $entity The managed entity. + * @param string $property The property in many-to-many relation. + * + * @throws EntityException + * @throws AnnotationException + * @throws LightQLException + */ + private function _fetchManyToMany(&$entity, $property) + { + $manyToMany = Annotations::ofProperty($entity, $property, "@manyToMany"); + $column = Annotations::ofProperty($entity, $property, "@column"); + + $mappedPropertyName = null; + + /** @var Entity $referencedEntity */ + $referencedEntity = new $manyToMany[0]->entity; + foreach ($referencedEntity->getColumns() as $p => $c) { + if ($c->isManyToMany) { + $mappedManyToMany = Annotations::ofProperty($referencedEntity, $p, "@manyToMany"); + if ($mappedManyToMany[0]->crossTable === $manyToMany[0]->crossTable) { + $mappedPropertyName = $p; + break; + } + } + } + unset($referencedEntity); + + if ($mappedPropertyName === null) { + throw new EntityException("Unable to find a suitable property with a @manyToMany annotation in the entity \"{$manyToMany[0]->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->parseValue($entity->get($column[0]->name)))) + ->joinArray( + "{$referencedEntityAnnotations[0]->table}.*", + array( + array( + "side" => "LEFT", + "table" => $referencedEntityAnnotations[0]->table, + "cond" => "{$manyToMany[0]->crossTable}.{$mappedPropertyManyToManyAnnotation[0]->referencedColumn} = {$referencedEntityAnnotations[0]->table}.{$mappedPropertyColumnAnnotation[0]->name}" + ) + ) + ); + + $entity->{$property} = $this->_parseRawEntities( + $results, + $manyToMany[0]->entity, + $referencedEntityAnnotations + ); + } + + /** + * Fetch data for a one-to-many relation. + * + * @param IEntity $entity The managed entity. + * @param string $property The property in one-to-many relation. + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + */ + private function _fetchOneToMany(&$entity, $property) + { + $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}\"."); + } + + $lightql = $this->entityManager->getLightQL(); + + $result = $lightql + ->from($referencedEntityAnnotations[0]->table) + ->where(array("{$referencedEntityAnnotations[0]->table}.{$oneToMany[0]->referencedColumn}" => $lightql->parseValue($entity->get($column[0]->name)))) + ->selectFirst("{$referencedEntityAnnotations[0]->table}.*"); + + $className = $oneToMany[0]->entity; + + $entity->{$property} = $result; + + if ($result !== null) { + $entity->{$property} = $this->_parseRawEntity( + $result, + $oneToMany[0]->entity, + $referencedEntityAnnotations + ); + } + } + + /** + * Fetch data for a many-to-one relation. + * + * @param IEntity $entity The managed entity. + * @param string $property The property in many-to-one relation. + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + */ + private function _fetchManyToOne(&$entity, $property) + { + $manyToOne = Annotations::ofProperty($entity, $property, "@manyToOne"); + $column = Annotations::ofProperty($entity, $property, "@column"); + $referencedEntityAnnotations = Annotations::ofClass($manyToOne[0]->entity, "@entity"); + + $mappedPropertyName = $this->_resolveMappedPropertyName($manyToOne[0]->entity, "OneToMany", $manyToOne[0]->referencedColumn); + + if ($mappedPropertyName === null) { + throw new EntityException("Unable to find a suitable property with @oneToMany annotation in the entity \"{$manyToOne[0]->entity}\"."); + } + + $lightql = $this->entityManager->getLightQL(); + + $results = $lightql + ->from($referencedEntityAnnotations[0]->table) + ->where(array("{$referencedEntityAnnotations[0]->table}.{$manyToOne[0]->referencedColumn}" => $lightql->parseValue($entity->get($column[0]->name)))) + ->selectArray("{$referencedEntityAnnotations[0]->table}.*"); + + $className = $manyToOne[0]->entity; + + $entity->{$property} = array_map(function($item) use ($entity, $mappedPropertyName, $className) { + $e = new $className($item); + $e->{$mappedPropertyName} = &$entity; + return $e; + }, $results); + } + + /** + * Fetch data for a one-to-one relation. + * + * @param IEntity $entity The managed entity. + * @param string $property The property in one-to-one relation. + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + */ + private function _fetchOneToOne(&$entity, $property) + { + $oneToOne = Annotations::ofProperty($entity, $property, "@oneToOne"); + $column = Annotations::ofProperty($entity, $property, "@column"); + $referencedEntityAnnotations = Annotations::ofClass($oneToOne[0]->entity, "@entity"); + + $mappedPropertyName = $this->_resolveMappedPropertyName($oneToOne[0]->entity, "OneToOne", $oneToOne[0]->referencedColumn); + + if ($mappedPropertyName === null) { + throw new EntityException("Unable to find a suitable property with @oneToOne annotation in the entity \"{$oneToOne[0]->entity}\"."); + } + + $lightql = $this->entityManager->getLightQL(); + + $result = $lightql + ->from($referencedEntityAnnotations[0]->table) + ->where(array("{$referencedEntityAnnotations[0]->table}.{$oneToOne[0]->referencedColumn}" => $lightql->parseValue($entity->get($column[0]->name)))) + ->selectFirst("{$referencedEntityAnnotations[0]->table}.*"); + + $className = $oneToOne[0]->entity; + + $entity->{$property} = $result; + + if ($result !== null) { + $entity->{$property} = $this->_parseRawEntity($result, $className, $referencedEntityAnnotations); + } + } + + /** + * 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. + * + * @return string|null + */ + private function _resolveMappedPropertyName(string $entityClass, string $check, string $column): ?string + { + $mappedPropertyName = null; + + /** @var Entity $referencedEntity */ + $referencedEntity = new $entityClass; + foreach ($referencedEntity->getColumns() as $p => $c) { + if ($c->{"is{$check}"} && $c->getName() === $column) { + $mappedPropertyName = $p; + break; + } + } + unset($referencedEntity); + + return $mappedPropertyName; + } + + /** + * Parse a set of raw data to a set of Entities. + * + * @param array $rawEntities The set of raw entities data provided fromm database. + * @param string $className The name of the entity class. + * @param array $annotations The set of entity annotations. + * + * @return Entity[] + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + * @throws \ReflectionException + * + * @internal This method is intended to be used only internally by LightQL itself. + */ + public function _parseRawEntities($rawEntities, $className, $annotations): array + { + $entities = array(); + + foreach ($rawEntities as $rawEntity) { + array_push($entities, $this->_parseRawEntity($rawEntity, $className, $annotations)); + } + + return $entities; + } + + /** + * Parses raw data to Entity. + * + * @param array|null $rawEntity Raw entity data provided from database. + * @param string $className The name of the entity class. + * @param EntityAnnotation[] $annotations The set of entity annotations. + * + * @return Entity|null + * + * @throws AnnotationException + * @throws EntityException + * @throws LightQLException + * @throws \ReflectionException + */ + private function _parseRawEntity($rawEntity, $className, $annotations): ?Entity + { + if ($rawEntity === null) + return null; + + /** @var IPrimaryKey $pkClass */ + $pkClassReflection = null; + $pkClass = null; + + if (Annotations::classHasAnnotation($className, "@pkClass")) { + $pkClassAnnotation = Annotations::ofClass($className, "@pkClass"); + + if (\is_subclass_of($pkClassAnnotation[0]->name, IPrimaryKey::class)) { + $pkClassReflection = new \ReflectionClass($pkClassAnnotation[0]->name); + + $pkClass = $pkClassReflection->newInstance(); + } else { + throw new EntityException("The primary key class of this entity doesn't implement the IPrimaryKey interface"); + } + } + + if ($pkClass !== null) { + $properties = $pkClassReflection->getProperties(T_PUBLIC); + + foreach ($properties as $property) { + if (Annotations::propertyHasAnnotation($pkClass, $property->name, "@column")) { + $columnAnnotations = Annotations::ofProperty($pkClass, $property->name, "@column"); + $pkClass->{$property->name} = $rawEntity[$columnAnnotations[0]->name]; + } + } + } + + $reflection = new \ReflectionClass($className); + + /** @var Entity $entity */ + $entity = $reflection->newInstance($rawEntity); + + if ($annotations[0]->fetchMode === Entity::FETCH_EAGER) { + $properties = $reflection->getProperties(T_PUBLIC); + + foreach ($properties as $property) { + if (Annotations::propertyHasAnnotation($entity, $property->name, "@id") && $pkClass !== null) { + $entity->{$property->name} = $pkClass; + } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@manyToMany") && !is_array($property->getValue($entity))) { + $this->_fetchManyToMany($entity, $property->name); + } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@oneToMany") && !($property->getValue($entity) instanceof IEntity)) { + $this->_fetchOneToMany($entity, $property->name); + } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@manyToOne") && !is_array($property->getValue($entity))) { + $this->_fetchManyToOne($entity, $property->name); + } elseif (Annotations::propertyHasAnnotation($entity, $property->name, "@oneToOne") && !($property->getValue($entity) instanceof IEntity)) { + $this->_fetchOneToOne($entity, $property->name); + } + } + } + + return $entity; + } +} diff --git a/src/LightQL/Sessions/GenericFacade.php b/src/LightQL/Sessions/GenericFacade.php new file mode 100644 index 0000000..7a7ce58 --- /dev/null +++ b/src/LightQL/Sessions/GenericFacade.php @@ -0,0 +1,322 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Sessions; + +use ElementaryFramework\LightQL\Entities\EntitiesCollection; +use ElementaryFramework\LightQL\Entities\GenericEntity; +use ElementaryFramework\LightQL\Entities\IEntity; +use ElementaryFramework\LightQL\Exceptions\EntityException; +use ElementaryFramework\LightQL\Exceptions\FacadeException; +use ElementaryFramework\LightQL\Exceptions\LightQLException; +use ElementaryFramework\LightQL\LightQL; +use ElementaryFramework\LightQL\Persistence\PersistenceUnit; + +final class GenericFacade implements IFacade +{ + /** + * The persistence unit used by this instance. + * + * @var PersistenceUnit + */ + private $_persistenceUnit; + + /** + * The managed LightQL instance. + * + * @var LightQL + */ + private $_lightql; + + /** + * GenericFacade constructor. + * + * @param PersistenceUnit $persistenceUnit The persistence unit to use with this GenericFacade. + * + * @throws LightQLException + */ + public function __construct(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(), + "port" => $this->_persistenceUnit->getPort() + ) + ); + } + + /** + * Creates an entity. + * + * @param IEntity $entity The entity to create. + * + * @throws FacadeException + * @throws EntityException + */ + public function create(IEntity &$entity) + { + if ($entity instanceof GenericEntity) { + $this->_lightql->beginTransaction(); + try { + $this->_lightql + ->from($entity->getName()) + ->insert($entity->getData()); + + $this->_lightql->commit(); + } catch (\Exception $e) { + $this->_lightql->rollback(); + + throw new EntityException($e->getMessage()); + } + } else { + throw new FacadeException("Only GenericEntity instances can be used with GenericFacade."); + } + } + + /** + * Edit an entity. + * + * @param IEntity $entity The entity to edit. + * @throws EntityException + * @throws FacadeException + */ + public function edit(IEntity &$entity) + { + if ($entity instanceof GenericEntity) { + $this->_lightql->beginTransaction(); + try { + $this->_lightql + ->from($entity->getName()) + ->where(array( + $entity->getPk() => $this->_lightql->parseValue($entity->get($entity->getPk())) + )) + ->update($entity->getData()); + + $this->_lightql->commit(); + } catch (\Exception $e) { + $this->_lightql->rollback(); + + throw new EntityException($e->getMessage()); + } + } else { + throw new FacadeException("Only GenericEntity instances can be used with GenericFacade."); + } + } + + /** + * Delete an entity. + * + * @param IEntity $entity The entity to delete. + * @throws EntityException + * @throws FacadeException + */ + function delete(IEntity &$entity) + { + if ($entity instanceof GenericEntity) { + $this->_lightql->beginTransaction(); + try { + $this->_lightql + ->from($entity->getName()) + ->where(array( + $entity->getPk() => $this->_lightql->parseValue($entity->get($entity->getPk())) + )) + ->delete(); + + $this->_lightql->commit(); + } catch (\Exception $e) { + $this->_lightql->rollback(); + + throw new EntityException($e->getMessage()); + } + } else { + throw new FacadeException("Only GenericEntity instances can be used with GenericFacade."); + } + } + + /** + * Find an entity. + * + * This method is unavailable. Use findGeneric instead. + * + * @param mixed $id The id of the entity to find + * + * @return IEntity|null + * + * @throws FacadeException + */ + public function find($id): ?IEntity + { + throw new FacadeException("The \"find\" method is unavailable in GenericFacade, use \"findGeneric\" instead."); + } + + /** + * Find an entity. + * + * @param string $table The name of the table + * @param string $pk The name of the column with primary key property + * @param mixed $id The pk value of the entity to find + * + * @return IEntity|null + * + * @throws LightQLException + */ + public function findGeneric(string $table, string $pk, $id): ?IEntity + { + $raw = $this->_lightql + ->from($table) + ->where(array($pk => $this->_lightql->parseValue($id))) + ->selectFirst(); + + if ($raw === null) + return null; + + return new GenericEntity($table, $pk, $raw); + } + + /** + * Find all entities. + * + * This method is unavailable. Use findGeneric instead. + * + * @return EntitiesCollection + * + * @throws FacadeException + */ + public function findAll(): EntitiesCollection + { + throw new FacadeException("The \"findAll\" method is unavailable in GenericFacade, use \"findAllGeneric\" instead."); + } + + /** + * Find all entities. + * + * @param string $table The name of the table + * @param string $pk The name of the column with primary key property + * + * @return EntitiesCollection + * + * @throws LightQLException + */ + public function findAllGeneric(string $table, string $pk): EntitiesCollection + { + $rawEntities = $this->_lightql + ->from($table) + ->selectArray(); + + return new EntitiesCollection( + array_map(function ($raw) use ($table, $pk) { + return new GenericEntity($table, $pk, $raw); + }, $rawEntities) + ); + } + + /** + * Find all entities in the given range. + * + * This method is unavailable. Use findRangeGeneric instead. + * + * @param int $start The starting offset. + * @param int $length The number of entities to find. + * + * @return EntitiesCollection + * + * @throws FacadeException + */ + public function findRange(int $start, int $length): EntitiesCollection + { + throw new FacadeException("The \"findRange\" method is unavailable in GenericFacade, use \"findRangeGeneric\" instead."); + } + + /** + * Find all entities in the given range. + * + * @param string $table The name of the table + * @param string $pk The name of the column with primary key property + * @param int $start The starting offset. + * @param int $length The number of entities to find. + * + * @return EntitiesCollection + * + * @throws LightQLException + */ + public function findRangeGeneric(string $table, string $pk, int $start, int $length): EntitiesCollection + { + $rawEntities = $this->_lightql + ->from($table) + ->limit($start, $length) + ->selectArray(); + + return new EntitiesCollection( + array_map(function ($raw) use ($table, $pk) { + return new GenericEntity($table, $pk, $raw); + }, $rawEntities) + ); + } + + /** + * Count the number of entities. + * + * This method is unavailable. Use countGeneric instead. + * + * @return int + * + * @throws FacadeException + */ + public function count(): int + { + throw new FacadeException("The \"count\" method is unavailable in GenericFacade, use \"countGeneric\" instead."); + } + + /** + * Count the number of entities. + * + * @param string $table The name of the table + * + * @return int + * + * @throws LightQLException + */ + public function countGeneric(string $table): int + { + return $this->_lightql + ->from($table) + ->count(); + } +} \ No newline at end of file diff --git a/src/LightQL/Sessions/IFacade.php b/src/LightQL/Sessions/IFacade.php index 9f6f2db..789eb5a 100644 --- a/src/LightQL/Sessions/IFacade.php +++ b/src/LightQL/Sessions/IFacade.php @@ -32,7 +32,8 @@ namespace ElementaryFramework\LightQL\Sessions; -use ElementaryFramework\LightQL\Entities\Entity; +use ElementaryFramework\LightQL\Entities\EntitiesCollection; +use ElementaryFramework\LightQL\Entities\IEntity; /** * IFacade @@ -49,39 +50,39 @@ interface IFacade /** * Creates an entity. * - * @param Entity $entity The entity to create. + * @param IEntity $entity The entity to create. */ - function create(Entity &$entity); + function create(IEntity &$entity); /** * Edit an entity. * - * @param Entity $entity The entity to edit. + * @param IEntity $entity The entity to edit. */ - function edit(Entity &$entity); + function edit(IEntity &$entity); /** * Delete an entity. * - * @param Entity $entity The entity to delete. + * @param IEntity $entity The entity to delete. */ - function delete(Entity &$entity); + function delete(IEntity &$entity); /** * Find an entity. * * @param mixed $id The id of the entity to find * - * @return Entity + * @return IEntity|null */ - function find($id): Entity; + function find($id): ?IEntity; /** * Find all entities. * - * @return Entity[] + * @return EntitiesCollection */ - function findAll(): array; + function findAll(): EntitiesCollection; /** * Find all entities in the given range. @@ -89,9 +90,9 @@ function findAll(): array; * @param int $start The starting offset. * @param int $length The number of entities to find. * - * @return Entity[] + * @return EntitiesCollection */ - function findRange(int $start, int $length): array; + function findRange(int $start, int $length): EntitiesCollection; /** * Count the number of entities. diff --git a/src/LightQL/Sessions/IFacadeListener.php b/src/LightQL/Sessions/IFacadeListener.php new file mode 100644 index 0000000..78e0cec --- /dev/null +++ b/src/LightQL/Sessions/IFacadeListener.php @@ -0,0 +1,96 @@ + + * @copyright 2018 Aliens Group, Inc. + * @license MIT + * @version 1.0.0 + * @link http://lightql.na2axl.tk + */ + +namespace ElementaryFramework\LightQL\Sessions; + +use ElementaryFramework\LightQL\Entities\IEntity; + +/** + * IFacadeListener + * + * Provide methods for all entity facade listeners. + * + * @category Sessions + * @package LightQL + * @author Nana Axel + * @link http://lightql.na2axl.tk/docs/api/LightQL/Sessions/IFacadeListener + */ +interface IFacadeListener +{ + /** + * An entity will be created. + * + * @param IEntity $entity The entity to create. + * + * @return bool true if we can execute the query, false to cancel the entity creation. + */ + function beforeCreate(IEntity &$entity): bool; + + /** + * An entity was just created. + * + * @param IEntity $entity The created entity. + */ + function onCreate(IEntity $entity); + + /** + * An entity will be edited. + * + * @param IEntity $entity The entity to edit. + * + * @return bool true if we can execute the query, false to cancel the entity edition. + */ + function beforeEdit(IEntity &$entity): bool; + + /** + * An entity was just edited. + * + * @param IEntity $entity The entity to edit. + */ + function onEdit(IEntity $entity); + + /** + * An entity will be deleted. + * + * @param IEntity $entity The entity to delete. + * + * @return bool true if we can execute the query, false to cancel the entity deletion. + */ + function beforeDelete(IEntity &$entity): bool; + + /** + * An entity was just deleted. + * + * @param IEntity $entity The entity to delete. + */ + function onDelete(IEntity $entity); +} \ No newline at end of file