diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..21b1699 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,103 @@ +filter: + excluded_paths: [vendor/*] + +checks: + php: + verify_property_names: true + verify_access_scope_valid: true + variable_existence: true + useless_calls: true + use_statement_alias_conflict: true + use_self_instead_of_fqcn: true + uppercase_constants: true + unused_variables: true + unused_properties: true + unused_parameters: true + unused_methods: true + unreachable_code: true + too_many_arguments: true + symfony_request_injection: true + switch_fallthrough_commented: false + sql_injection_vulnerabilities: true + single_namespace_per_use: true + simplify_boolean_return: true + side_effects_or_types: true + security_vulnerabilities: true + return_doc_comments: true + return_doc_comment_if_not_inferrable: true + require_scope_for_properties: true + require_scope_for_methods: true + require_php_tag_first: true + psr2_switch_declaration: true + psr2_class_declaration: true + properties_in_camelcaps: true + precedence_mistakes: true + precedence_in_conditions: true + phpunit_assertions: true + php5_style_constructor: true + parse_doc_comments: true + parameters_in_camelcaps: true + parameter_non_unique: true + parameter_doc_comments: true + param_doc_comment_if_not_inferrable: true + overriding_private_members: true + optional_parameters_at_the_end: true + one_class_per_file: true + no_unnecessary_if: true + no_unnecessary_final_modifier: true + no_underscore_prefix_in_properties: true + no_underscore_prefix_in_methods: true + no_trailing_whitespace: true + no_short_variable_names: + minimum: '2' + no_short_open_tag: true + no_short_method_names: + minimum: '3' + no_property_on_interface: true + no_non_implemented_abstract_methods: true + no_goto: true + no_global_keyword: true + no_exit: true + no_eval: true + no_error_suppression: true + no_empty_statements: true + no_commented_out_code: true + no_debug_code: true + no_duplicate_arguments: true + newline_at_end_of_file: true + argument_type_checks: true + assignment_of_null_return: true + avoid_aliased_php_functions: true + avoid_closing_tag: true + avoid_conflicting_incrementers: true + avoid_duplicate_types: true + avoid_multiple_statements_on_same_line: true + avoid_superglobals: true + fix_doc_comments: true + non_commented_empty_catch_block: false + line_length: + max_length: '120' + encourage_single_quotes: true + classes_in_camel_caps: true + more_specific_types_in_doc_comments: true + avoid_unnecessary_concatenation: true + remove_extra_empty_lines: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: false + order_alphabetically: false + fix_line_ending: true + function_in_camel_caps: true + +tools: + external_code_coverage: true + php_code_sniffer: + config: + standard: "PSR2" + +build_failure_conditions: + - 'patches.new.exists' # No patches allowed + - 'issues.label("coding-style").new.exists' # No coding style issues allowed + - 'issues.label("documentation").new.exists' # No documentation issues allowed + - 'project.metric("scrutinizer.test_coverage", < 0.60)' # Code Coverage drops below 60% diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6119342 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: php +php: + - '5.6' + - '7.0' + +env: + - SYMFONY_VERSION=2.3.* + - SYMFONY_VERSION=2.7.* + - SYMFONY_VERSION=2.8.* + - SYMFONY_VERSION=3.0.* + - SYMFONY_VERSION=3.1.* + +before_script: + - composer self-update + - composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update + - composer install --no-interaction + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_success: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + - bash <(curl -s https://codecov.io/bash) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4359a0b --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +.SILENT: +.PHONY: help + +## Colors +COLOR_RESET = \033[0m +COLOR_INFO = \033[32m +COLOR_COMMENT = \033[33m + +## Help +help: + printf "${COLOR_COMMENT}Usage:${COLOR_RESET}\n" + printf " make \n\n" + printf "${COLOR_COMMENT}Available targets:${COLOR_RESET}\n" + awk '/^[a-zA-Z\-\_0-9\.@]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf " ${COLOR_INFO}%-16s${COLOR_RESET} %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) + +## Install +install: + composer install + +## Run tests +test: + vendor/bin/phpunit + +## Loop unit tests +test-loop: + while true; \ + do vendor/bin/phpunit; \ + read continue; \ + done; + +## Code coverage +coverage: + vendor/bin/phpunit --coverage-text diff --git a/README.md b/README.md index 5e88fda..5239c28 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,163 @@ -# algolia-specification-bundle -This bundle integrates algolia-specification with Symfony +# Algolia specification bundle + +This bundle integrates [algolia-specification](git@github.com:gbprod/algolia-specification.git) with Symfony. + +![stability-wip](https://img.shields.io/badge/stability-work_in_progress-lightgrey.svg) + +[![Build Status](https://travis-ci.org/gbprod/algolia-specification-bundle.svg?branch=master)](https://travis-ci.org/gbprod/algolia-specification-bundle) +[![codecov](https://codecov.io/gh/gbprod/algolia-specification-bundle/branch/master/graph/badge.svg)](https://codecov.io/gh/gbprod/algolia-specification-bundle) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/gbprod/algolia-specification-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/gbprod/algolia-specification-bundle/?branch=master) +[![Dependency Status](https://www.versioneye.com/user/projects/574a9c9ace8d0e004130d337/badge.svg)](https://www.versioneye.com/user/projects/574a9c9ace8d0e004130d337) + +[![Latest Stable Version](https://poser.pugx.org/gbprod/algolia-specification-bundle/v/stable)](https://packagist.org/packages/gbprod/algolia-specification) +[![Total Downloads](https://poser.pugx.org/gbprod/algolia-specification-bundle/downloads)](https://packagist.org/packages/gbprod/algolia-specification) +[![Latest Unstable Version](https://poser.pugx.org/gbprod/algolia-specification-bundle/v/unstable)](https://packagist.org/packages/gbprod/algolia-specification) +[![License](https://poser.pugx.org/gbprod/algolia-specification-bundle/license)](https://packagist.org/packages/gbprod/algolia-specification) + +## Installation + +Download bundle using [composer](https://getcomposer.org/) : + +```bash +composer require gbprod/algolia-specification-bundle +``` + +Declare in your `app/AppKernel.php` file: + +```php +isSellable() + && $candidate->expirationDate() > new \DateTime('now') + ; + } +} +``` + +### Create a query factory + +```php +query()->bool() + ->addMust() + $qb->query()->term(['available' => "0"]), + ) + ; + } +} +``` + +## Configuration + +### Declare your Factory + +```yaml +// src/GBProd/Acme/AcmeBundle/Resource/config/service.yml + +services: + acme.algolia.query_factory.is_available: + class: GBProd\Acme\Infrastructure\Algolia\QueryFactory\Product\IsAvailableFactory + tags: + - { name: algolia.query_factory, specification: GBProd\Acme\CoreDomain\Specification\Product\IsAvailable } +``` + +### Inject handler in your repository class + +```yaml +// src/GBProd/Acme/AcmeBundle/Resource/config/service.yml + +services: + acme.product_repository: + class: GBProd\Acme\Infrastructure\Product\AlgoliaProductRepository + arguments: + - "@algolia.client" + - "@gbprod.algolia_specification_handler" +``` + +```php +client = $client; + $this->handler = $handler; + } + + public function findSatisfying(Specification $specification) + { + $type = $this + ->getIndex('catalog') + ->getType('product') + ; + + $query = $this->handler->handle($specification, new QueryBuilder()); + + return $type->search($query); + } +} +``` + +### Usage + +```php +findSatisfying( + new AndX( + new IsAvailable(), + new IsLowStock() + ) +); +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7b8ead0 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "gbprod/algolia-specification-bundle", + "description": "This bundle integrates algolia-specification with Symfony", + "type": "bundle", + "require": { + "php": ">=5.6", + "gbprod/algolia-specification": "^0.1", + "symfony/framework-bundle": "^2.3|^3.0", + "symfony/expression-language": "^2.3|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.5" + }, + "license": "MIT", + "authors": [ + { + "name": "Gilles", + "email": "contact@gb-prod.fr" + } + ], + "minimum-stability": "stable", + "autoload": { + "psr-4": { + "GBProd\\": "src/", + "Tests\\GBProd\\": "tests/" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..101cd3a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + + + + + + tests + + + + + + src + + + diff --git a/src/AlgoliaSpecificationBundle/AlgoliaSpecificationBundle.php b/src/AlgoliaSpecificationBundle/AlgoliaSpecificationBundle.php new file mode 100644 index 0000000..a922445 --- /dev/null +++ b/src/AlgoliaSpecificationBundle/AlgoliaSpecificationBundle.php @@ -0,0 +1,24 @@ + + */ +class AlgoliaSpecificationBundle extends Bundle +{ + /** + * {inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + $container->addCompilerPass(new QueryFactoryPass()); + } +} diff --git a/src/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtension.php b/src/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtension.php new file mode 100644 index 0000000..950c567 --- /dev/null +++ b/src/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtension.php @@ -0,0 +1,32 @@ + + */ +class AlgoliaSpecificationExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader( + $container, + new FileLocator(__DIR__.'/../Resources/config') + ); + + $loader->load('services.yml'); + } +} diff --git a/src/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPass.php b/src/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPass.php new file mode 100644 index 0000000..eef98b1 --- /dev/null +++ b/src/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPass.php @@ -0,0 +1,44 @@ + + */ +class QueryFactoryPass implements CompilerPassInterface +{ + /** + * {inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->has('gbprod.algolia_specification_handler')) { + throw new \Exception('Missing gbprod.algolia_specification_handler definition'); + } + + $handler = $container->findDefinition('gbprod.algolia_specification_handler'); + + $factories = $container->findTaggedServiceIds('algolia.query_factory'); + + foreach ($factories as $id => $tags) { + foreach ($tags as $attributes) { + if (!isset($attributes['specification'])) { + throw new \Exception( + 'The algolia.query_factory tag must always have a "specification" attribute' + ); + } + + $handler->addMethodCall( + 'registerFactory', + [$attributes['specification'], new Reference($id)] + ); + } + } + } +} diff --git a/src/AlgoliaSpecificationBundle/DependencyInjection/Configuration.php b/src/AlgoliaSpecificationBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..e728b0f --- /dev/null +++ b/src/AlgoliaSpecificationBundle/DependencyInjection/Configuration.php @@ -0,0 +1,25 @@ + + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $treeBuilder->root('algolia_specification_bundle'); + + return $treeBuilder; + } +} diff --git a/src/AlgoliaSpecificationBundle/Resources/config/services.yml b/src/AlgoliaSpecificationBundle/Resources/config/services.yml new file mode 100644 index 0000000..eb8d032 --- /dev/null +++ b/src/AlgoliaSpecificationBundle/Resources/config/services.yml @@ -0,0 +1,8 @@ +services: + gbprod.algolia_specification_registry: + class: GBProd\AlgoliaSpecification\Registry + + gbprod.algolia_specification_handler: + class: GBProd\AlgoliaSpecification\Handler + arguments: + - "@gbprod.algolia_specification_registry" diff --git a/tests/AlgoliaSpecificationBundle/AlgoliaSpecificationBundleTest.php b/tests/AlgoliaSpecificationBundle/AlgoliaSpecificationBundleTest.php new file mode 100644 index 0000000..d63a39b --- /dev/null +++ b/tests/AlgoliaSpecificationBundle/AlgoliaSpecificationBundleTest.php @@ -0,0 +1,36 @@ + + */ +class AlgoliaSpecificationBundleTest extends \PHPUnit_Framework_TestCase +{ + public function testConstruction() + { + $bundle = new AlgoliaSpecificationBundle(); + + $this->assertInstanceOf(AlgoliaSpecificationBundle::class, $bundle); + $this->assertInstanceOf(Bundle::class, $bundle); + } + + public function testBuildAddCompilerPass() + { + $container = $this->prophesize(ContainerBuilder::class); + $container + ->addCompilerPass(new QueryFactoryPass()) + ->shouldBeCalled() + ; + + $bundle = new AlgoliaSpecificationBundle(); + $bundle->build($container->reveal()); + } +} diff --git a/tests/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtensionTest.php b/tests/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtensionTest.php new file mode 100644 index 0000000..c1d7f21 --- /dev/null +++ b/tests/AlgoliaSpecificationBundle/DependencyInjection/AlgoliaSpecificationExtensionTest.php @@ -0,0 +1,55 @@ + + */ +class AlgoliaSpecificationExtensionTest extends \PHPUnit_Framework_TestCase +{ + private $extension; + private $container; + + protected function setUp() + { + $this->extension = new AlgoliaSpecificationExtension(); + + $this->container = new ContainerBuilder(); + $this->container->registerExtension($this->extension); + + $this->container->loadFromExtension($this->extension->getAlias()); + $this->container->compile(); + } + + public function testLoadHasServices() + { + $this->assertTrue( + $this->container->has('gbprod.algolia_specification_registry') + ); + + $this->assertTrue( + $this->container->has('gbprod.algolia_specification_handler') + ); + } + + public function testLoadRegistry() + { + $registry = $this->container->get('gbprod.algolia_specification_registry'); + + $this->assertInstanceOf(Registry::class, $registry); + } + + public function testLoadHandler() + { + $handler = $this->container->get('gbprod.algolia_specification_handler'); + + $this->assertInstanceOf(Handler::class, $handler); + } +} diff --git a/tests/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPassTest.php b/tests/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPassTest.php new file mode 100644 index 0000000..7b5f5bd --- /dev/null +++ b/tests/AlgoliaSpecificationBundle/DependencyInjection/Compiler/QueryFactoryPassTest.php @@ -0,0 +1,103 @@ + + */ +class QueryFactoryPassTest extends \PHPUnit_Framework_TestCase +{ + public function testThrowExceptionIfNoHandlerDefinition() + { + $pass = new QueryFactoryPass(); + + $this->expectException(\Exception::class); + + $pass->process(new ContainerBuilder()); + } + + public function testDoNothingIfNoTaggedServices() + { + $pass = new QueryFactoryPass(); + $container = $this->createContainerWithHandler(); + + $pass->process($container); + + $calls = $container + ->getDefinition('gbprod.algolia_specification_handler') + ->getMethodCalls() + ; + + $this->assertEmpty($calls); + } + + private function createContainerWithHandler() + { + $container = new ContainerBuilder(); + + $container->setDefinition( + 'gbprod.algolia_specification_handler', + new Definition(Handler::class) + ); + + return $container; + } + + public function testThrowExceptionIfTagHasNoSpecification() + { + $pass = new QueryFactoryPass(); + + $container = $this->createContainerWithHandler(); + $container + ->register('factory', \stdClass::class) + ->addTag('algolia.query_factory') + ; + + $this->expectException(\Exception::class); + $pass->process($container); + } + + public function testAddMethodCalls() + { + $pass = new QueryFactoryPass(); + + $container = $this->createContainerWithHandler(); + $container + ->register('factory1', 'Factory1') + ->addTag('algolia.query_factory', ['specification' => 'Specification1']) + ; + + $container + ->register('factory2', 'Factory2') + ->addTag('algolia.query_factory', ['specification' => 'Specification2']) + ; + + $pass->process($container); + + $calls = $container + ->getDefinition('gbprod.algolia_specification_handler') + ->getMethodCalls() + ; + + $this->assertCount(2, $calls); + + $this->assertEquals('registerFactory', $calls[0][0]); + $this->assertEquals('Specification1', $calls[0][1][0]); + $this->assertInstanceOf(Reference::class, $calls[0][1][1]); + $this->assertEquals('factory1', $calls[0][1][1]); + + + $this->assertEquals('registerFactory', $calls[1][0]); + $this->assertEquals('Specification2', $calls[1][1][0]); + $this->assertInstanceOf(Reference::class, $calls[1][1][1]); + $this->assertEquals('factory2', $calls[1][1][1]); + } +} diff --git a/tests/AlgoliaSpecificationBundle/DependencyInjection/ConfigurationTest.php b/tests/AlgoliaSpecificationBundle/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..413d7ed --- /dev/null +++ b/tests/AlgoliaSpecificationBundle/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,31 @@ + + */ +class ConfigurationTest extends \PHPUnit_Framework_TestCase +{ + public function testGetConfigTreeBuilder() + { + $configuration = new Configuration(); + + $tree = $configuration->getConfigTreeBuilder(); + + $this->assertInstanceOf( + TreeBuilder::class, + $tree + ); + + $this->assertEquals( + 'algolia_specification_bundle', + $tree->buildTree()->getName() + ); + } +}