diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a4c885 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +/tests export-ignore +/examples export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml export-ignore +/phpcs.xml export-ignore +/captainhook.json export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45f673b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.idea +/composer.lock +/vendor +/.phpunit* +/tests/_output +/tests/_reports \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..752aa0b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Andreas Leathley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cf4a68 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +Squirrel Entities Integration for Symfony +========================================= + +Integration of [squirrelphp/entities](https://github.com/squirrelphp/entities) into Symfony through bundle configuration, also needs [squirrelphp/queries-bundle](https://github.com/squirrelphp/queries-bundle). + +nstallation +------------ + +``` +composer require squirrelphp/entities-bundle +``` + +Configuration +------------- + +Enable the bundle in your AppKernel by adding `Squirrel\EntitiesBundle\SquirrelEntitiesBundle` to the list of bundles. + +Configure the directories where the bundle will look for repositories like this in Symfony / YAML: + + squirrel_entities: + directories: + - '%kernel.project_dir%/src' + - '%kernel.project_dir%/possibleOtherDirectory' + +It will go through these directories recusively, finding all entities and possible repositories and creating services for the repositories. + +Look at `Squirrel\EntitiesBundle\DependencyInjection\ContainerExtension` to get more details about how it works, and check out the `squirrel_repositories_generate` bin command in [squirrelphp/entities](https://github.com/squirrelphp/entities) to generate repositories from entities which are then autoloaded by this bundle. + +More documentation will follow! \ No newline at end of file diff --git a/captainhook.json b/captainhook.json new file mode 100644 index 0000000..f2532d5 --- /dev/null +++ b/captainhook.json @@ -0,0 +1,60 @@ +{ + "commit-msg": { + "enabled": true, + "actions": [ + { + "action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Beams", + "options": { + "subjectLength": 50, + "bodyLineLength": 72 + }, + "conditions": [] + } + ] + }, + "pre-push": { + "enabled": false, + "actions": [] + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting", + "options": [], + "conditions": [] + }, + { + "action": "vendor/bin/phpunit", + "options": [], + "conditions": [] + }, + { + "action": "vendor/bin/phpstan analyse src --level=7", + "options": [], + "conditions": [] + }, + { + "action": "vendor/bin/phpcs --standard=psr2 --extensions=php src tests", + "options": [], + "conditions": [] + } + ] + }, + "prepare-commit-msg": { + "enabled": false, + "actions": [] + }, + "post-commit": { + "enabled": false, + "actions": [] + }, + "post-merge": { + "enabled": false, + "actions": [] + }, + "post-checkout": { + "enabled": false, + "actions": [] + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fb30353 --- /dev/null +++ b/composer.json @@ -0,0 +1,60 @@ +{ + "name": "squirrelphp/entities-bundle", + "type": "library", + "description": "Symfony integration of squirrelphp/entities - automatic integration of generated repositories for existing entities.", + "keywords": [ + "php", + "mysql", + "pgsql", + "sqlite", + "database", + "abstraction", + "entities", + "repositories", + "symfony", + "bundle" + ], + "homepage": "https://github.com/squirrelphp/entities-bundle", + "license": "MIT", + "authors": [ + { + "name": "Andreas Leathley", + "email": "andreas.leathley@panaxis.ch" + } + ], + "require": { + "php": "^7.2", + "symfony/dependency-injection": "^4.0", + "symfony/http-kernel": "^4.0", + "symfony/finder": "^4.0", + "symfony/config": "^4.0", + "squirrelphp/queries-bundle": "^0.5", + "squirrelphp/entities": "^0.2" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpstan/phpstan": "^0.11.5", + "phpunit/phpunit": "^8.0", + "squizlabs/php_codesniffer": "^3.0", + "captainhook/plugin-composer": "^4.0" + }, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { + "Squirrel\\EntitiesBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Squirrel\\EntitiesBundle\\Tests\\": "tests/" + } + }, + "scripts": { + "phpstan": "vendor/bin/phpstan analyse src --level=7", + "phpunit": "vendor/bin/phpunit --colors=always", + "phpcs": "vendor/bin/phpcs --standard=psr2 --extensions=php src tests", + "codecoverage": "vendor/bin/phpunit --coverage-html tests/_reports" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..727c6d5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + excludes_analyse: + - src/DependencyInjection/Configuration.php \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9f44502 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + + tests + + + + + + src + + + diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..f3bce81 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,55 @@ +alias = $alias; + } + + /** + * @inheritdoc + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root($this->alias); + + /* + * The only configuration options are: + * + * - directories (there are no default directories) + * - table_names (overwriting annotated table names) + * - connection_names (overwriting annotated connection names) + */ + $rootNode + ->fixXmlConfig('directory', 'directories') + ->fixXmlConfig('table_name', 'table_names') + ->fixXmlConfig('connection_name', 'connection_names') + ->children() + ->arrayNode('directories') + ->prototype('scalar') + ->end() + ->end() + ->arrayNode('table_names') + ->useAttributeAsKey('class') + ->prototype('scalar') + ->end() + ->end() + ->arrayNode('connection_names') + ->useAttributeAsKey('class') + ->prototype('scalar') + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/ContainerExtension.php b/src/DependencyInjection/ContainerExtension.php new file mode 100644 index 0000000..978a422 --- /dev/null +++ b/src/DependencyInjection/ContainerExtension.php @@ -0,0 +1,231 @@ +getConfiguration([], $container); + $config = $this->processConfiguration($configuration, $configs); + + // Load explicitely configured directories + $directories = $config['directories'] ?? []; + + if (count($directories) > 0) { + // Initialize entity processor to find repository config + $entityProcessor = new EntityProcessor(new AnnotationReader()); + + // Looks through a PHP file to find possible entity classes + $findEntityClasses = new FindClassesWithAnnotation(); + + // Collect used connections - to create transaction services further down + $connectionNames = []; + + // Go through directories + foreach ($directories as $directory) { + // Find the files in the directory + $sourceFinder = new Finder(); + $sourceFinder->in($directory)->files()->name('*.php'); + + // Go through files which were found + foreach ($sourceFinder as $file) { + // Safety check because Finder can return false if the file was not found + if ($file->getRealPath()===false) { + throw new \InvalidArgumentException('File in source directory not found'); + } + + // Get file contents + $fileContents = \file_get_contents($file->getRealPath()); + + // Another safety check because file_get_contents can return false if the file was not found + if ($fileContents===false) { + throw new \InvalidArgumentException('File in source directory could not be retrieved'); + } + + // Get all possible entity classes with our annotation + $classes = $findEntityClasses->__invoke($fileContents); + + // Go through the possible entity classes + foreach ($classes as $class) { + // Divvy up the namespace and the class name + $namespace = $class[0]; + $className = $class[1]; + + // Get repository config as object + $repositoryConfig = $entityProcessor->process($namespace . '\\' . $className); + + // Repository config found - this is an entity + if (isset($repositoryConfig)) { + // Connection can be overwritten in configuration + $connectionName = + isset($config['connection_names'][$className]) ? + $config['connection_names'][$className] : + $repositoryConfig->getConnectionName(); + + // Table name can be overwritten in configuration + $tableName = + isset($config['table_names'][$className]) ? + $config['table_names'][$className] : + $repositoryConfig->getTableName(); + + // Create repository config definition + $repositoryConfigDef = new Definition( + RepositoryConfig::class, + [ + $connectionName, + $tableName, + $repositoryConfig->getTableToObjectFields(), + $repositoryConfig->getObjectToTableFields(), + $repositoryConfig->getObjectClass(), + $repositoryConfig->getObjectTypes(), + $repositoryConfig->getObjectTypesNullable(), + true, + ] + ); + + // Specific connection set for this repository + if (strlen($connectionName) > 0) { + $dbReference = 'squirrel.connection.' . $connectionName; + $connectionNames[] = $connectionName; + } else { // No connection set - use default connection + $dbReference = DBInterface::class; + $connectionNames[] = DBInterface::class; + } + + // ReadOnly builder repository exists + if (class_exists($className . 'RepositoryReadOnly')) { + $builderRepositoryReadOnlyDefinition = new Definition( + $className . 'RepositoryReadOnly', + [ + new Definition(RepositoryReadOnly::class, [ + new Reference($dbReference), + $repositoryConfigDef, + ]), + ] + ); + + $container->setDefinition( + $className . 'RepositoryReadOnly', + $builderRepositoryReadOnlyDefinition + ); + + // Writeable builder repository exists + if (class_exists($className . 'RepositoryWriteable')) { + $builderRepositoryWriteableDefinition = new Definition( + $className . 'RepositoryWriteable', + [ + new Definition(RepositoryWriteable::class, [ + new Reference($dbReference), + $repositoryConfigDef, + ]), + ] + ); + + $container->setDefinition( + $className . 'RepositoryWriteable', + $builderRepositoryWriteableDefinition + ); + } + } + } + } + } + } + + // Create a transaction service for each connection + foreach ($connectionNames as $connectionName) { + if ($connectionName !== DBInterface::class) { + $serviceName = 'squirrel.transaction.' . $connectionName; + $connectionService = 'squirrel.connection.' . $connectionName; + } else { + $serviceName = TransactionInterface::class; + $connectionService = DBInterface::class; + } + + $container->setDefinition( + $serviceName, + new Definition(Transaction::class, [new Reference($connectionService)]) + ); + } + + // Query handler read-only definition + $rawMultiRepositoryReadOnlyDef = new Definition(MultiRepositoryReadOnly::class); + $rawMultiRepositoryReadOnlyDef->setPublic(false); + + // Query handler read-and-write definition + $rawMultiRepositoryReadWriteDef = new Definition(MultiRepositoryWriteable::class); + $rawMultiRepositoryReadWriteDef->setPublic(false); + + // Make query handler available through raw query interface names + $container->setDefinition(MultiRepositoryReadOnlyInterface::class, $rawMultiRepositoryReadOnlyDef); + $container->setDefinition(MultiRepositoryWriteableInterface::class, $rawMultiRepositoryReadWriteDef); + + // Multi-repository read-only query handler definition + $multiRepositoryReadOnlyDef = new Definition( + MultiRepositoryBuilderReadOnly::class, + [$rawMultiRepositoryReadOnlyDef] + ); + $multiRepositoryReadOnlyDef->setPublic(false); + + // Make multi-repository read-only query handler available through interface + $container->setDefinition(MultiRepositoryBuilderReadOnlyInterface::class, $multiRepositoryReadOnlyDef); + + // Multi-repository read-write query handler definition + $multiRepositoryReadWriteDef = new Definition( + MultiRepositoryBuilderWriteable::class, + [$rawMultiRepositoryReadWriteDef] + ); + $multiRepositoryReadWriteDef->setPublic(false); + + // Make multi-repository read-write query handler available through interface + $container->setDefinition(MultiRepositoryBuilderWriteableInterface::class, $multiRepositoryReadWriteDef); + } + } + + /** + * @inheritdoc + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($this->getAlias()); + } + + /** + * @inheritdoc + */ + public function getAlias() + { + return 'squirrel_entities'; + } +} diff --git a/src/SquirrelEntitiesBundle.php b/src/SquirrelEntitiesBundle.php new file mode 100644 index 0000000..d72c9a3 --- /dev/null +++ b/src/SquirrelEntitiesBundle.php @@ -0,0 +1,14 @@ +