From afa2f42c0867202e38b0269df775d1d23e4c3b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9C=C3=A9?= Date: Mon, 1 Oct 2018 00:34:46 +0200 Subject: [PATCH] List filters form (#62) Adds list form filters --- README.md | 77 +++++++ doc/res/img/list-form-filters.png | Bin 0 -> 39154 bytes .../ListFormFiltersConfigPass.php | 196 ++++++++++++++++++ .../PostQueryBuilderSubscriber.php | 41 +++- src/Helper/ListFormFiltersHelper.php | 62 ++++++ src/Resources/config/services.xml | 14 ++ .../public/js/easyadmin-extension.js | 9 + .../public/stylesheet/easyadmin-extension.css | 3 + .../translations/EasyAdminBundle.en.xlf | 22 +- .../translations/EasyAdminBundle.es.xlf | 22 +- .../translations/EasyAdminBundle.fr.xlf | 26 ++- .../translations/EasyAdminBundle.hu.xlf | 22 +- .../translations/EasyAdminBundle.it.xlf | 22 +- src/Resources/views/default/layout.html.twig | 5 + src/Resources/views/default/list.html.twig | 78 ++++++- src/Twig/ListFormFiltersExtension.php | 28 +++ tests/Controller/ListFormFiltersTest.php | 41 ++++ tests/Fixtures/AbstractTestCase.php | 3 +- .../App/config/config_list_form_filters.yml | 15 ++ .../DataFixtures/ORM/LoadProducts.php | 18 +- .../Entity/FunctionalTests/Product.php | 43 ++++ 21 files changed, 723 insertions(+), 24 deletions(-) create mode 100644 doc/res/img/list-form-filters.png create mode 100644 src/Configuration/ListFormFiltersConfigPass.php create mode 100644 src/Helper/ListFormFiltersHelper.php create mode 100644 src/Resources/public/stylesheet/easyadmin-extension.css create mode 100644 src/Twig/ListFormFiltersExtension.php create mode 100644 tests/Controller/ListFormFiltersTest.php create mode 100644 tests/Fixtures/App/config/config_list_form_filters.yml diff --git a/README.md b/README.md index 1af00c2..211b31b 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,83 @@ If you have defined your own admin controllers, make them extend EasyAdminExtens Features -------- +### List filters form + +Add filters on list views by configuration. + +Consider following Animation entity using such [ValueListTrait](https://github.com/alterphp/components/blob/master/src/AlterPHP/Component/Behavior/ValueListTrait) : + +```php +class Animation +{ + use ValueListTrait; + + /** + * @var string + * + * @ORM\Id + * @ORM\Column(type="guid") + */ + private $id; + + /** + * @var bool + * + * @ORM\Column(type="boolean", nullable=false) + */ + private $enabled; + + /** + * @var string + * + * @ORM\Column(type="string", length=31) + */ + protected $status; + + /** + * @var string + * + * @ORM\Column(type="string", length=31, nullable=false) + */ + private $type; + + /** + * @var Organization + * + * @ORM\ManyToOne(targetEntity="App\Entity\Organization", inversedBy="animations") + * @ORM\JoinColumn(nullable=false) + */ + private $organization; + + const STATUS_DRAFT = 'draft'; + const STATUS_PUBLISHED = 'published'; + const STATUS_OPEN = 'open'; + const STATUS_ACTIVE = 'active'; + const STATUS_CLOSED = 'closed'; + const STATUS_ARCHIVED = 'archived'; +} +``` + +Define your filters under `list`.`form_filters` entity configuration. Automatic guesser set up a ChoiceType for filters mapped on boolean (NULL, true, false) and string class properties. ChoiceType for string properties requires either a `choices` label/value array in `type_options` of a `choices_static_callback` static callable that returns label/value choices list. + + +```yaml +easy_admin: + entities: + Animation: + class: App\Entity\Animation + list: + form_filters: + - enabled + - { property: type, type_options: { choices: { Challenge: challenge, Event: event } } } + - { property: status, type_options: { choices_static_callback: [getValuesList, [status, true]] } } + - organization +``` + +Let's see the result ! + +![Embedded list example](/doc/res/img/list-form-filters.png) + ### Filter list and search on request parameters * EasyAdmin allows filtering list with `dql_filter` configuration entry. But this is not dynamic and must be configured as an apart list in `easy_admin` configuration.* diff --git a/doc/res/img/list-form-filters.png b/doc/res/img/list-form-filters.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1eb7b897f1482adae6f36aad626dd03987c75b GIT binary patch literal 39154 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 Vn&@9|e|O$FYRcM5)sL)S{x=sgjd}n8 literal 0 HcmV?d00001 diff --git a/src/Configuration/ListFormFiltersConfigPass.php b/src/Configuration/ListFormFiltersConfigPass.php new file mode 100644 index 0000000..5c6a735 --- /dev/null +++ b/src/Configuration/ListFormFiltersConfigPass.php @@ -0,0 +1,196 @@ +doctrine = $doctrine; + } + + /** + * @param array $backendConfig + * + * @return array + */ + public function process(array $backendConfig): array + { + if (!isset($backendConfig['entities'])) { + return $backendConfig; + } + + foreach ($backendConfig['entities'] as $entityName => $entityConfig) { + if (!isset($entityConfig['list']['form_filters'])) { + continue; + } + + $formFilters = array(); + + foreach ($entityConfig['list']['form_filters'] as $i => $formFilter) { + // Detects invalid config node + if (!is_string($formFilter) && !is_array($formFilter)) { + throw new \RuntimeException( + sprintf( + 'The values of the "form_filters" option for the list view of the "%s" entity can only be strings or arrays.', + $entityConfig['class'] + ) + ); + } + + // Key mapping + if (is_string($formFilter)) { + $filterConfig = array('property' => $formFilter); + } else { + if (!array_key_exists('property', $formFilter)) { + throw new \RuntimeException( + sprintf( + 'One of the values of the "form_filters" option for the "list" view of the "%s" entity does not define the mandatory option "property".', + $entityConfig['class'] + ) + ); + } + + $filterConfig = $formFilter; + } + + $this->configureFilter($entityConfig['class'], $filterConfig); + + // If type is not configured at this steps => not guessable + if (!isset($filterConfig['type'])) { + continue; + } + + $formFilters[$filterConfig['property']] = $filterConfig; + } + + // set form filters config and form ! + $backendConfig['entities'][$entityName]['list']['form_filters'] = $formFilters; + } + + return $backendConfig; + } + + private function configureFilter(string $entityClass, array &$filterConfig) + { + // No need to guess type + if (isset($filterConfig['type'])) { + return; + } + + $em = $this->doctrine->getManagerForClass($entityClass); + $entityMetadata = $em->getMetadataFactory()->getMetadataFor($entityClass); + + // Not able to guess type + if ( + !$entityMetadata->hasField($filterConfig['property']) + && !$entityMetadata->hasAssociation($filterConfig['property']) + ) { + return; + } + + if ($entityMetadata->hasField($filterConfig['property'])) { + $this->configureFieldFilter( + $entityClass, $entityMetadata->getFieldMapping($filterConfig['property']), $filterConfig + ); + } elseif ($entityMetadata->hasAssociation($filterConfig['property'])) { + $this->configureAssociationFilter( + $entityClass, $entityMetadata->getAssociationMapping($filterConfig['property']), $filterConfig + ); + } + } + + private function configureFieldFilter(string $entityClass, array $fieldMapping, array &$filterConfig) + { + switch ($fieldMapping['type']) { + case 'boolean': + $filterConfig['type'] = ChoiceType::class; + $defaultFilterConfigTypeOptions = array( + 'choices' => array( + 'list_form_filters.default.boolean.true' => true, + 'list_form_filters.default.boolean.false' => false, + ), + 'choice_translation_domain' => 'EasyAdminBundle', + ); + break; + case 'string': + $filterConfig['type'] = ChoiceType::class; + $defaultFilterConfigTypeOptions = array( + 'multiple' => true, + 'choices' => $this->getChoiceList($entityClass, $filterConfig['property'], $filterConfig), + 'attr' => array('data-widget' => 'select2'), + ); + break; + default: + return; + } + + // Merge default type options when defined + if (isset($defaultFilterConfigTypeOptions)) { + $filterConfig['type_options'] = array_merge( + $defaultFilterConfigTypeOptions, + isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array() + ); + } + } + + private function configureAssociationFilter(string $entityClass, array $associationMapping, array &$filterConfig) + { + // To-One (EasyAdminAutocompleteType) + if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { + $filterConfig['type'] = EasyAdminAutocompleteType::class; + $filterConfig['type_options'] = array_merge( + array( + 'class' => $associationMapping['targetEntity'], + 'multiple' => true, + 'attr' => array('data-widget' => 'select2'), + ), + isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array() + ); + } + } + + private function getChoiceList(string $entityClass, string $property, array &$filterConfig) + { + if (isset($filterConfig['type_options']['choices'])) { + $choices = $filterConfig['type_options']['choices']; + unset($filterConfig['type_options']['choices']); + + return $choices; + } + + if (!isset($filterConfig['type_options']['choices_static_callback'])) { + throw new \RuntimeException( + sprintf( + 'Choice filter field "%s" for entity "%s" must provide either a static callback method returning choice list or choices option.', + $property, + $entityClass + ) + ); + } + + $callableParams = array(); + if (is_string($filterConfig['type_options']['choices_static_callback'])) { + $callable = array($entityClass, $filterConfig['type_options']['choices_static_callback']); + } else { + $callable = array($entityClass, $filterConfig['type_options']['choices_static_callback'][0]); + $callableParams = $filterConfig['type_options']['choices_static_callback'][1]; + } + unset($filterConfig['type_options']['choices_static_callback']); + + return forward_static_call_array($callable, $callableParams); + } +} diff --git a/src/EventListener/PostQueryBuilderSubscriber.php b/src/EventListener/PostQueryBuilderSubscriber.php index 18f4c3f..ea5da94 100644 --- a/src/EventListener/PostQueryBuilderSubscriber.php +++ b/src/EventListener/PostQueryBuilderSubscriber.php @@ -35,6 +35,7 @@ public function onPostListQueryBuilder(GenericEvent $event) if ($event->hasArgument('request')) { $this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('filters', array())); + $this->applyFormFilters($queryBuilder, $event->getArgument('request')->get('form_filters', array())); } } @@ -53,7 +54,7 @@ public function onPostSearchQueryBuilder(GenericEvent $event) } /** - * Applies filters on queryBuilder. + * Applies request filters on queryBuilder. * * @param QueryBuilder $queryBuilder * @param array $filters @@ -72,12 +73,48 @@ protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filter continue; } // Sanitize parameter name - $parameter = 'filter_'.str_replace('.', '_', $field); + $parameter = 'request_filter_'.str_replace('.', '_', $field); $this->filterQueryBuilder($queryBuilder, $field, $parameter, $value); } } + /** + * Applies form filters on queryBuilder. + * + * @param QueryBuilder $queryBuilder + * @param array $filters + */ + protected function applyFormFilters(QueryBuilder $queryBuilder, array $filters = array()) + { + foreach ($filters as $field => $value) { + $value = $this->filterEasyadminAutocompleteValue($value); + // Empty string and numeric keys is considered as "not applied filter" + if (is_int($field) || '' === $value) { + continue; + } + // Add root entity alias if none provided + $field = false === strpos($field, '.') ? $queryBuilder->getRootAlias().'.'.$field : $field; + // Checks if filter is directly appliable on queryBuilder + if (!$this->isFilterAppliable($queryBuilder, $field)) { + continue; + } + // Sanitize parameter name + $parameter = 'form_filter_'.str_replace('.', '_', $field); + + $this->filterQueryBuilder($queryBuilder, $field, $parameter, $value); + } + } + + private function filterEasyadminAutocompleteValue($value) + { + if (!is_array($value) || !isset($value['autocomplete']) || 1 !== count($value)) { + return $value; + } + + return $value['autocomplete']; + } + /** * Filters queryBuilder. * diff --git a/src/Helper/ListFormFiltersHelper.php b/src/Helper/ListFormFiltersHelper.php new file mode 100644 index 0000000..f16ebff --- /dev/null +++ b/src/Helper/ListFormFiltersHelper.php @@ -0,0 +1,62 @@ +formFactory = $formFactory; + $this->requestStack = $requestStack; + } + + public function getListFiltersForm(array $formFilters): FormInterface + { + if (!isset($this->listFiltersForm)) { + $formBuilder = $this->formFactory->createNamedBuilder('form_filters'); + + foreach ($formFilters as $name => $config) { + $formBuilder->add( + $name, + isset($config['type']) ? $config['type'] : null, + array_merge( + array('required' => false), + $config['type_options'] + ) + ); + } + + $this->listFiltersForm = $formBuilder->setMethod('GET')->getForm(); + $this->listFiltersForm->handleRequest($this->requestStack->getCurrentRequest()); + } + + return $this->listFiltersForm; + } +} diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index ed3270c..131bdb1 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -34,6 +34,20 @@ + + + + + + + + + + + + + + diff --git a/src/Resources/public/js/easyadmin-extension.js b/src/Resources/public/js/easyadmin-extension.js index eb8a050..e355df3 100644 --- a/src/Resources/public/js/easyadmin-extension.js +++ b/src/Resources/public/js/easyadmin-extension.js @@ -75,4 +75,13 @@ $(function() { $('#modal-confirm').modal({ backdrop: true, keyboard: true }); }); + + // Deal with panel-heading toggling collapsible panel-body + // (@see https://stackoverflow.com/questions/33725181/bootstrap-using-panel-heading-to-collapse-panel-body) + $('.panel-heading[data-toggle^="collapse"]').click(function(){ + var target = $(this).attr('data-target'); + $(target).collapse('toggle'); + }).children().click(function(e) { + e.stopPropagation(); + }); }); diff --git a/src/Resources/public/stylesheet/easyadmin-extension.css b/src/Resources/public/stylesheet/easyadmin-extension.css new file mode 100644 index 0000000..a206186 --- /dev/null +++ b/src/Resources/public/stylesheet/easyadmin-extension.css @@ -0,0 +1,3 @@ +#list-form-filters { + margin: 10px 0; +} diff --git a/src/Resources/translations/EasyAdminBundle.en.xlf b/src/Resources/translations/EasyAdminBundle.en.xlf index 90f48ed..957c082 100644 --- a/src/Resources/translations/EasyAdminBundle.en.xlf +++ b/src/Resources/translations/EasyAdminBundle.en.xlf @@ -7,7 +7,27 @@ open.new_tab Open in a new tab - + + + list_form_filters.heading_title + Filters + + + list_form_filters.heading_expandcollapse + Expand / Collapse + + + list_form_filters.submit + Filter + + + list_form_filters.default.boolean.true + Yes + + + list_form_filters.default.boolean.false + No + confirm_modal.content diff --git a/src/Resources/translations/EasyAdminBundle.es.xlf b/src/Resources/translations/EasyAdminBundle.es.xlf index 6e778d8..60b1ce6 100644 --- a/src/Resources/translations/EasyAdminBundle.es.xlf +++ b/src/Resources/translations/EasyAdminBundle.es.xlf @@ -7,7 +7,27 @@ open.new_tab Abrir en una nueva pestaña - + + + list_form_filters.heading_title + Filtros + + + list_form_filters.heading_expandcollapse + Abrir / Cerrar + + + list_form_filters.submit + Filtro + + + list_form_filters.default.boolean.true + Si + + + list_form_filters.default.boolean.false + No + confirm_modal.content diff --git a/src/Resources/translations/EasyAdminBundle.fr.xlf b/src/Resources/translations/EasyAdminBundle.fr.xlf index d4691ce..71438e7 100644 --- a/src/Resources/translations/EasyAdminBundle.fr.xlf +++ b/src/Resources/translations/EasyAdminBundle.fr.xlf @@ -2,13 +2,33 @@ - + open.new_tab Ouvrir dans un nouvel onglet - - + + + list_form_filters.heading_title + Filtres + + + list_form_filters.heading_expandcollapse + Ouvrir / Fermer + + + list_form_filters.submit + Filtrer + + + list_form_filters.default.boolean.true + Oui + + + list_form_filters.default.boolean.false + Non + + confirm_modal.content Êtes-vous sûr(e) ? diff --git a/src/Resources/translations/EasyAdminBundle.hu.xlf b/src/Resources/translations/EasyAdminBundle.hu.xlf index 445478e..93fa99e 100644 --- a/src/Resources/translations/EasyAdminBundle.hu.xlf +++ b/src/Resources/translations/EasyAdminBundle.hu.xlf @@ -7,7 +7,27 @@ open.new_tab Megnyitás új lapon - + + + list_form_filters.heading_title + Szűrők + + + list_form_filters.heading_expandcollapse + Nyitás / Bezárás + + + list_form_filters.submit + Szűrő + + + list_form_filters.default.boolean.true + Igen + + + list_form_filters.default.boolean.false + Nem + confirm_modal.content diff --git a/src/Resources/translations/EasyAdminBundle.it.xlf b/src/Resources/translations/EasyAdminBundle.it.xlf index 800c2a0..4ba80ec 100644 --- a/src/Resources/translations/EasyAdminBundle.it.xlf +++ b/src/Resources/translations/EasyAdminBundle.it.xlf @@ -7,7 +7,27 @@ open.new_tab Apri in una nuova scheda - + + + list_form_filters.heading_title + Filtri + + + list_form_filters.heading_expandcollapse + Apri / Chiudi + + + list_form_filters.submit + Filtro + + + list_form_filters.default.boolean.true + Si + + + list_form_filters.default.boolean.false + Non + confirm_modal.content diff --git a/src/Resources/views/default/layout.html.twig b/src/Resources/views/default/layout.html.twig index fee9a10..e9307e5 100644 --- a/src/Resources/views/default/layout.html.twig +++ b/src/Resources/views/default/layout.html.twig @@ -1,5 +1,10 @@ {% extends '@BaseEasyAdmin/default/layout.html.twig' %} +{% block head_stylesheets %} + {{ parent() }} + +{% endblock %} + {% block head_javascript %} {{ parent() }} diff --git a/src/Resources/views/default/list.html.twig b/src/Resources/views/default/list.html.twig index 54044f7..e81e84b 100644 --- a/src/Resources/views/default/list.html.twig +++ b/src/Resources/views/default/list.html.twig @@ -5,6 +5,18 @@ filters: requestFilters }) %} +{% block request_parameters_as_hidden %} + {% for field, value in requestFilters %} + {% if value is iterable %} + {% for val in value %} + + {% endfor %} + {% else %} + + {% endif %} + {% endfor %} +{% endblock %} + {% block content_title_wrapper %}

{{ block('content_title') }} @@ -46,16 +58,62 @@ {{ parent() }} {% endblock %} -{# Adds request filters to the serach form #} +{# Adds request filters to the search form #} {% block search_form %} - {% for field, value in requestFilters %} - {% if value is iterable %} - {% for val in value %} - - {% endfor %} - {% else %} - - {% endif %} - {% endfor %} + {{ block('request_parameters_as_hidden') }} + {{ parent() }} +{% endblock %} + +{% block list_form_filters %} + {% if _entity_config.list.form_filters is defined and _entity_config.list.form_filters is not empty %} + {% set list_form_filters = list_form_filters(_entity_config.list.form_filters) %} +
+
+ {{ 'list_form_filters.heading_title'|trans(_trans_parameters, 'EasyAdminBundle') }} + + {{ 'list_form_filters.heading_expandcollapse'|trans(_trans_parameters, 'EasyAdminBundle') }} + +
+
+
+
+ {% form_theme list_form_filters '@EasyAdmin/form/bootstrap_3_layout.html.twig' %} + {{ block('request_parameters_as_hidden') }} + + + + + + + {% for field in list_form_filters %} +
{{ form_row(field) }}
+ {% endfor %} +
+
+
+ +
+
+
+
+
+ {% endif %} +{% endblock %} + +{# Display FILTERS form if defined #} +{% block main %} + {{ block('list_form_filters') }} + {{ parent() }} +{% endblock %} + +{% block body_javascript %} {{ parent() }} + {{ include('@EasyAdmin/default/includes/_select2_widget.html.twig') }} + {% endblock %} diff --git a/src/Twig/ListFormFiltersExtension.php b/src/Twig/ListFormFiltersExtension.php new file mode 100644 index 0000000..05b349d --- /dev/null +++ b/src/Twig/ListFormFiltersExtension.php @@ -0,0 +1,28 @@ +listFiltersHelper = $listFiltersHelper; + } + + public function getFunctions() + { + return array( + new TwigFunction('list_form_filters', array($this, 'getListFormFilters')), + ); + } + + public function getListFormFilters(array $filters) + { + return $this->listFiltersHelper->getListFiltersForm($filters)->createView(); + } +} diff --git a/tests/Controller/ListFormFiltersTest.php b/tests/Controller/ListFormFiltersTest.php new file mode 100644 index 0000000..9707726 --- /dev/null +++ b/tests/Controller/ListFormFiltersTest.php @@ -0,0 +1,41 @@ +initClient(array('environment' => 'list_form_filters')); + } + + public function testListFiltersAreDisplaid() + { + $crawler = $this->requestListView('Product'); + + $listFormFiltersCrawler = $crawler->filter('#list-form-filters'); + + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_oddEven[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_category_autocomplete[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_replenishmentType[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_enabled')->count()); + } + + public function testFormSingleFilterIsApplied() + { + $crawler = $this->requestListView('Product', array(), array('enabled' => false)); + + $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + } + + public function testFormSingleEasyadminAutocomplteFilterIsApplied() + { + $crawler = $this->requestListView('Product', array(), array('category' => array('autocomplete' => 1))); + + $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + } +} diff --git a/tests/Fixtures/AbstractTestCase.php b/tests/Fixtures/AbstractTestCase.php index 07075ff..d98e4aa 100644 --- a/tests/Fixtures/AbstractTestCase.php +++ b/tests/Fixtures/AbstractTestCase.php @@ -75,13 +75,14 @@ protected function getBackendHomepage() /** * @return Crawler */ - protected function requestListView($entityName = 'Category', array $requestFilters = array()) + protected function requestListView($entityName = 'Category', array $requestFilters = array(), array $formFilters = array()) { return $this->getBackendPage(array( 'action' => 'list', 'entity' => $entityName, 'view' => 'list', 'filters' => $requestFilters, + 'form_filters' => $formFilters, )); } diff --git a/tests/Fixtures/App/config/config_list_form_filters.yml b/tests/Fixtures/App/config/config_list_form_filters.yml new file mode 100644 index 0000000..f74e024 --- /dev/null +++ b/tests/Fixtures/App/config/config_list_form_filters.yml @@ -0,0 +1,15 @@ +imports: + - { resource: config.yml } + +easy_admin: + entities: + Category: + class: AppTestBundle\Entity\FunctionalTests\Category + Product: + class: AppTestBundle\Entity\FunctionalTests\Product + list: + form_filters: + - { property: oddEven, type_options: { choices: {Odd: odd, Even: even} } } + - { property: replenishmentType, type_options: { choices_static_callback: [getReplenishmentTypeValues, [true]] } } + - enabled + - category diff --git a/tests/Fixtures/AppTestBundle/DataFixtures/ORM/LoadProducts.php b/tests/Fixtures/AppTestBundle/DataFixtures/ORM/LoadProducts.php index b915876..2bec0cb 100644 --- a/tests/Fixtures/AppTestBundle/DataFixtures/ORM/LoadProducts.php +++ b/tests/Fixtures/AppTestBundle/DataFixtures/ORM/LoadProducts.php @@ -46,15 +46,17 @@ public function getOrder() public function load(ObjectManager $manager) { foreach (range(1, 100) as $i) { + $category = $i <= 10 ? $this->getReference('category-1') : $this->getRandomCategory(); $product = new Product(); - $product->setEnabled($i <= 90 ? true : false); - $product->setOddEven($i % 2 ? 'odd' : 'even'); + $product->setEnabled($i % 10 ? true : false); + $product->setOddEven($i % 4 ? 'odd' : 'even'); $product->setReference('ref'.str_pad($i, 6, '0', STR_PAD_LEFT)); $product->setName($this->getRandomName()); + $product->setReplenishmentType($this->getReplenishmentType()); $product->setPrice($this->getRandomPrice()); $product->setTags($this->getRandomTags()); $product->setEan($this->getRandomEan()); - $product->setCategory($this->getRandomCategory()); + $product->setCategory($category); $product->setDescription($this->getRandomDescription()); $product->setHtmlFeatures($this->getRandomHtmlFeatures()); $product->setPhone($i <= 10 ? null : '0123456789'); @@ -134,7 +136,8 @@ public function getRandomPrice() private function getRandomCategory() { - return $this->getReference('category-'.mt_rand(1, 100)); + // First category is reserved for first products (test purpose) + return $this->getReference('category-'.mt_rand(2, 100)); } public function getRandomDescription() @@ -152,4 +155,11 @@ public function getRandomHtmlFeatures() return '
  • '.implode('
  • ', array_slice($this->phrases, 0, $numFeatures)).'
'; } + + public function getReplenishmentType() + { + $replenishmentTypeValues = Product::getReplenishmentTypeValues(); + + return $replenishmentTypeValues[mt_rand(0, count($replenishmentTypeValues)-1)]; + } } diff --git a/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php b/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php index 25221f0..71e64a2 100644 --- a/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php +++ b/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php @@ -119,6 +119,14 @@ class Product */ protected $name; + /** + * The replenishment type of the product. + * + * @var string + * @ORM\Column(type="string") + */ + protected $replenishmentType; + /** * The description of the product. * @@ -150,6 +158,21 @@ class Product */ protected $phone; + public static function getReplenishmentTypeValues($withLabelsAsIndexes = false) + { + $replenishmentTypeValues = array( + 'replenishment_type.auto' => 'auto', + 'replenishment_type.trigger' => 'trigger', + 'replenishment_type.manual' => 'manual', + ); + + if (!$withLabelsAsIndexes) { + return array_values($replenishmentTypeValues); + } + + return $replenishmentTypeValues; + } + /** * Constructor of the Product class. * (Initialize some fields). @@ -303,6 +326,26 @@ public function getHtmlFeatures() return $this->htmlFeatures; } + /** + * Set replenishment type. + * + * @param string $replenishmentType + */ + public function setReplenishmentType($replenishmentType) + { + $this->replenishmentType = $replenishmentType; + } + + /** + * Get replenishment type. + * + * @return string + */ + public function getReplenishmentType() + { + return $this->replenishmentType; + } + /** * Set the product image. *