diff --git a/CHANGELOG.md b/CHANGELOG.md index 840b8e4f7..b69436c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,28 @@ and this project adheres to * [main] Add currentLocation to the User entity + add a page for selecting this location + add in the user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/133) * [activity] add user current location as default location for a new activity (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/133) +* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167) +* remove "search by phone configuration option": search by phone is now executed by default +* remplacer le classement par ordre alphabétique par un classement par ordre de pertinence, qui tient compte: + * de la présence d'une string avec le nom de la ville; + * de la similarité; + * du fait que la recherche commence par une partie du mot recherché +* ajouter la recherche par numéro de téléphone directement dans la barre de recherche et dans le formulaire recherche avancée; +* ajouter la recherche par date de naissance directement dans la barre de recherche; +* ajouter la recherche par ville dans la recherche avancée +* ajouter un lien vers le ménage dans les résultats de recherche +* ajouter l'id du parcours dans les résultats de recherche +* ajouter le demandeur dans les résultats de recherche +* ajout d'un bouton "recherche avancée" sur la page d'accueil +* [person] create an accompanying course: add client-side validation if no origin (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/210) +* [person] fix bounds for computing current person address: the new address appears immediatly +* [docgen] create a normalizer and serializer for normalization on doc format +* [person normalization] the key center is now "centers" and is an array. Empty array if no center + + +## Test releases + +### Test release 2021-11-15 * [main] fix adding multiple AddresseDeRelais (combine PickAddressType with ChillCollection) * [person]: do not suggest the current household of the person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/51) @@ -29,9 +51,6 @@ and this project adheres to * [person] do not ask for center any more on person creation * [3party] do not ask for center any more on 3party creation - -## Test releases - ### Test release 2021-11-08 * [person]: Display the name of a user when searching after a User (TMS) diff --git a/composer.json b/composer.json index 7b356f698..18722a420 100644 --- a/composer.json +++ b/composer.json @@ -91,7 +91,8 @@ }, "autoload-dev": { "psr-4": { - "App\\": "tests/app/src/" + "App\\": "tests/app/src/", + "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests" } }, "minimum-stability": "dev", diff --git a/docs/source/development/access_control_model.rst b/docs/source/development/access_control_model.rst index 58fe44319..e159f7fb6 100644 --- a/docs/source/development/access_control_model.rst +++ b/docs/source/development/access_control_model.rst @@ -23,15 +23,196 @@ Every time an entity is created, viewed or updated, the software check if the us The user must be granted access to the action on this particular entity, with this scope and center. +TL;DR +===== + +Resolve scope and center +------------------------ + +In a service, resolve the center and scope of an entity + +.. code-block:: php + + use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; + use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; + + + class MyService { + private ScopeResolverDispatcher $scopeResolverDispatcher; + private CenterResolverDispatcher $centerResolverDispatcher; + + public function myFunction($entity) { + /** @var null|Center[]|Center $center */ + $center = $this->centerResolverDispatcher->resolveCenter($entity); + // $center may be null, an array of center, or an instance of Center + + if ($this->scopeResolverDispatcher->isConcerned($entity) { + /** @var null|Scope[]|Scope */ + $scope = $this-scopeResolverDispatcher->resolveScope($entity); + // $scope may be null, an array of Scope, or an instance of Scope + } + + } + + } + +In twig template, resolve the center: + +.. code-block:: twig + + {# resolve a center #} + + {% if person|chill_resolve_center is not null%} + + {% if person|chill_resolve_center is iterable %} + {% set centers = person|chill_resolve_center %} + {% else %} + {% set centers = [ person|chill_resolve_center ] %} + {% endif %} + + + {{ 'Center'|trans|upper}} : + + {% for c in centers %} + {{ c.name|upper }} + {% if not loop.last %}, {% endif %} + {% endfor %} + {%- endif -%} + +In twig template, resolve the scope: + +.. code-block:: twig + + {% if entity|chill_is_scope_concerned %} + + {% if entity|chill_resolve_scope is iterable %} + {% set scopes = entity|chill_resolve_scope %} + {% else %} + {% set scopes = [ entity|chill_resolve_scope ] %} + {% endif %} + + Scopes : + {% for s in scopes %} + {{ c.name|localize_translatable_string }} + {% if not loop.last %}, {% endif %} + {% endfor %} + {%- endif -%} + +Build a ``Voter`` +----------------- + +.. code-block:: php + + security = $security; + + // we build here a voter helper. This will ease the operations below. + // when the authorization model is changed, it will be easy to make a different implementation + // of the helper, instead of writing all Voters + + $this->voterHelper = $voterHelperFactory + // create a builder with some context + ->generate(self::class) + // add the support of given roles for given class: + ->addCheckFor(Person::class, [self::SEE, self::CREATE]) + ->addCheckFor(PersonDocument::class, $this->getRoles()) + ->build(); + } + + + protected function supports($attribute, $subject) + { + return $this->voterHelper->supports($attribute, $subject); + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + // basic check + if (!$token->getUser() instanceof User) { + return false; + } + + // we first check the acl for associated elements. + // here, we must be able to see the person associated to the document: + if ($subject instanceof PersonDocument + && !$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) { + + // not possible to see the associated person ? Then, not possible to see the document! + return false; + } + + // the voter helper will implements the logic: + return $this->voterHelper->voteOnAttribute($attribute, $subject, $token); + } + + // all the method below are used to register roles into the admin part + public function getRoles() + { + return [ + self::CREATE, + self::SEE, + self::SEE_DETAILS, + self::UPDATE, + self::DELETE + ]; + } + + public function getRolesWithoutScope() + { + return array(); + } + + + public function getRolesWithHierarchy() + { + return ['PersonDocument' => $this->getRoles() ]; + } + } + + + + From an user point of view --------------------------- +========================== The software is design to allow fine tuned access rights for complicated installation and team structure. The administrators may also decide that every user has the right to see all resources, where team have a more simple structure. Here is an overview of the model. Chill can be multi-center -^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------- Chill is designed to be installed once for social center who work with multiple teams separated, or for social services's federation who would like to share the same installation of the software for all their members. @@ -42,7 +223,7 @@ Otherwise, it is not required to create multiple center: Chill can also work for Obviously, users working in the different centers are not allowed to see the entities (_persons_, _reports_, _activities_) of other centers. But users may be attached to multiple centers: consequently they will be able to see the entities of the multiple centers they are attached to. Inside center, scope divide team -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------- Users are attached to one or more center and, inside to those center, there may exists differents scopes. The aim of those _scopes_ is to divide the whole team of social worker amongst different departement, for instance: the social team, the psychologist team, the nurse team, the administrative team, ... Each team is granted of different rights amongst scope. For instance, the social team may not see the _activities_ of the psychologist team. The administrative team may see the date & time's activities, but is not allowed to see the detail of those entities (the personal notes, ...). @@ -52,8 +233,38 @@ As entities have only one scopes, if some entities must be shared across two dif Example: if some activities must be seen and updated between nurses and psychologists, the administrator will create a scope "nurse and psy" and add the ability for both team "nurse" and "psychologist" to "create", "see", and "update" the activities belonging to scope "nurse and psy". + +Where does the ``scope`` and ``center`` comes from ? +==================================================== + +Most often, scope and center comes from user's input: + +* when person is created, Chill asks the associated center to the user. Then, every entity associated to the user (Activity, ...) is associated to this center; +* when an entity is created, Chill asks the associated scope. + +The UI check the model before adding those input into form. If the user hae access to only one center or scope, this scope or center is filled automatically, and the UI does not ask the user. Most of the times, the user does not see "Pick a scope" and "Pick a center" inputs. + +Scope and Center are associated to entities through ``ManyToOne`` properties, which are then mapped to ``FOREIGN KEY`` in tables, ... + +But sometimes, this implementation does not fits the needs: + +* persons are associated to center *geographically*: the address of each person contains lat/lon coordinates, and the center is resolved from this coordinated; +* some would like to associated persons to multiple center, or one center; +* entities are associated to scope through the job reached by "creator" (an user); +* some would like not to use scope at all; +* … + +For this reasons, associated center and scopes must be resolved programmatically. The default implementation rely on the model association, as described above. But it becomes possible to change the behaviour on different implementations. + +Is my entity "concerned" by scopes ? +------------------------------------ + +Some entities are concerned by scope, some not. + +This is also programmatically resolved. + The concepts translated into code ------------------------------------ +=================================== .. figure:: /_static/access_control_model.png @@ -81,7 +292,7 @@ At each step of his lifetime (creation, view of the entity and eventually of his All those action are executed through symfony voters and helpers. How to check authorization ? ----------------------------- +============================ Just use the symfony way-of-doing, but do not forget to associate the entity you want to check access. For instance, in controller : @@ -100,34 +311,23 @@ Just use the symfony way-of-doing, but do not forget to associate the entity you And in template : -.. code-block:: html+jinja +.. code-block:: twig {{ if is_granted('CHILL_ENTITY_SEE', entity) %}print something{% endif %} -Retrieving reachable scopes and centers ----------------------------------------- +Retrieving reachable scopes and centers for a user +-------------------------------------------------- -The class :class:`Chill\\MainBundle\\Security\\Authorization\\AuthorizationHelper` helps you to get centers and scope reachable by a user. +The class :class:`Chill\\MainBundle\\Security\\Authorization\\AuthorizationHelperInterface` helps you to get centers and scope reachable by a user. Those methods are intentionnaly build to give information about user rights: - getReachableCenters: to get reachable centers for a user - getReachableScopes : to get reachable scopes for a user -.. note:: - - The service is reachable through the Depedency injection with the key `chill.main.security.authorization.helper`. Example : - - .. code-block:: php - - $helper = $container->get('chill.main.security.authorization.helper'); - -.. todo:: - - Waiting for a link between our api and this doc, we invite you to read the method signatures `here `_ Adding your own roles -===================== +--------------------- Extending Chill will requires you to define your own roles and rules for your entities. You will have to define your own voter to do so. @@ -152,7 +352,7 @@ To create your own roles, you should: Declare your role ------------------- +^^^^^^^^^^^^^^^^^^ To declare new role, implement the class :class:`Chill\\MainBundle\\Security\\ProvideRoleInterface`. @@ -212,69 +412,8 @@ Example of an implementation of :class:`Chill\\MainBundle\\Security\\ProvideRole } -Implement your voter --------------------- - -Inside this class, you might use the :class:`Chill\\MainBundle\\Security\\Authorization\\AuthorizationHelper` to check permission (do not re-invent the wheel). This is a real-world example: - -.. code-block:: php - - namespace Chill\ReportBundle\Security\Authorization; - use Chill\MainBundle\Security\Authorization\AbstractChillVoter; - use Chill\MainBundle\Security\Authorization\AuthorizationHelper; - - - class ReportVoter extends AbstractChillVoter - { - const CREATE = 'CHILL_REPORT_CREATE'; - const SEE = 'CHILL_REPORT_SEE'; - const UPDATE = 'CHILL_REPORT_UPDATE'; - - /** - * - * @var AuthorizationHelper - */ - protected $helper; - - public function __construct(AuthorizationHelper $helper) - { - $this->helper = $helper; - } - - protected function getSupportedAttributes() - { - return array(self::CREATE, self::SEE, self::UPDATE); - } - protected function getSupportedClasses() - { - return array('Chill\ReportBundle\Entity\Report'); - } - protected function isGranted($attribute, $report, $user = null) - { - if (! $user instanceof \Chill\MainBundle\Entity\User){ - - return false; - } - - return $this->helper->userHasAccess($user, $report, $attribute); - } - } - -Then, you will have to declare the service and tag it as a voter : - -.. code-block:: yaml - - services: - chill.report.security.authorization.report_voter: - class: Chill\ReportBundle\Security\Authorization\ReportVoter - arguments: - - "@chill.main.security.authorization.helper" - tags: - - { name: security.voter } - - Adding role hierarchy ---------------------- +^^^^^^^^^^^^^^^^^^^^^ You should prepend Symfony's security component directly from your code. @@ -312,3 +451,484 @@ You should prepend Symfony's security component directly from your code. } + +Implement your voter +^^^^^^^^^^^^^^^^^^^^ + +Most of the time, Voter will check that: + +1. The given role is reachable (= ``$attribute``) +2. for the given center, +3. and, if any, for the given role +4. if the entity is associated to another entity, this entity should be, at least, viewable by the user. + +Thats what we call the "autorization logic". But this logic may be replace by a new one, and developers should take care of it. + + +Then voter implementation should take care of: + +* check the access to associated entities. For instance, if an ``Activity`` is associated to a ``Person``, the voter should first check that the user can show the associated ``Person``; +* as far as possible, delegates the check for associated center, scopes, and check for authorization using the authorization logic. VoterHelper will ease the most common operation of this logic. + +This is an example of implementation: + + +.. code-block:: php + + security = $security; + + // we build here a voter helper. This will ease the operations below. + // when the authorization model is changed, it will be easy to make a different implementation + // of the helper, instead of writing all Voters + + $this->voterHelper = $voterHelperFactory + // create a builder with some context + ->generate(self::class) + // add the support of given roles for given class: + ->addCheckFor(Person::class, [self::SEE, self::CREATE]) + ->addCheckFor(PersonDocument::class, $this->getRoles()) + ->build(); + } + + + protected function supports($attribute, $subject) + { + return $this->voterHelper->supports($attribute, $subject); + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + // basic check + if (!$token->getUser() instanceof User) { + return false; + } + + // we first check the acl for associated elements. + // here, we must be able to see the person associated to the document: + if ($subject instanceof PersonDocument + && !$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) { + + // not possible to see the associated person ? Then, not possible to see the document! + return false; + } + + // the voter helper will implements the logic of checking: + // 1. that the center is reachable + // 2. for this given entity + // 3. for this given scope + // 4. and for the given role + return $this->voterHelper->voteOnAttribute($attribute, $subject, $token); + } + + public function getRoles() + { + // ... + } + + public function getRolesWithoutScope() + { + // ... + } + + + public function getRolesWithHierarchy() + { + // ... + } + } + +Then, you will have to declare the service and tag it as a voter : + +.. code-block:: yaml + + services: + chill.report.security.authorization.report_voter: + class: Chill\ReportBundle\Security\Authorization\ReportVoter + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: security.voter } + + +How to resolve scope and center programmatically ? +================================================== + +In a service, resolve the center and scope of an entity + +.. code-block:: php + + use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher; + use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; + + + class MyService { + private ScopeResolverDispatcher $scopeResolverDispatcher; + private CenterResolverDispatcher $centerResolverDispatcher; + + public function myFunction($entity) { + /** @var null|Center[]|Center $center */ + $center = $this->centerResolverDispatcher->resolveCenter($entity); + // $center may be null, an array of center, or an instance of Center + + if ($this->scopeResolverDispatcher->isConcerned($entity) { + /** @var null|Scope[]|Scope */ + $scope = $this-scopeResolverDispatcher->resolveScope($entity); + // $scope may be null, an array of Scope, or an instance of Scope + } + + } + + } + +In twig template, resolve the center: + +.. code-block:: twig + + {# resolve a center #} + + {% if person|chill_resolve_center is not null%} + + {% if person|chill_resolve_center is iterable %} + {% set centers = person|chill_resolve_center %} + {% else %} + {% set centers = [ person|chill_resolve_center ] %} + {% endif %} + + + {{ 'Center'|trans|upper}} : + + {% for c in centers %} + {{ c.name|upper }} + {% if not loop.last %}, {% endif %} + {% endfor %} + {%- endif -%} + +In twig template, resolve the scope: + +.. code-block:: twig + + {% if entity|chill_is_scope_concerned %} + + {% if entity|chill_resolve_scope is iterable %} + {% set scopes = entity|chill_resolve_scope %} + {% else %} + {% set scopes = [ entity|chill_resolve_scope ] %} + {% endif %} + + Scopes : + {% for s in scopes %} + {{ c.name|localize_translatable_string }} + {% if not loop.last %}, {% endif %} + {% endfor %} + {%- endif -%} + +What is the default implementation of Scope and Center resolver ? +----------------------------------------------------------------- + +By default, the implementation rely on association into entities. + +* implements ``Chill\MainBundle\Entity\HasCenterInterface`` on entities which have one or any center; +* implements ``Chill\MainBundle\Entity\HasCentersInterface`` on entities which have one, multiple or any centers; +* implements ``Chill\MainBundle\Entity\HasScopeInterface`` on entities which have one or any scope; +* implements ``Chill\MainBundle\Entity\HasScopesInterface`` on entities which have one or any scopes; + +Then, the default implementation will resolve the center and scope based on the implementation in your model. + +How to change the default behaviour ? +------------------------------------- + +Implements those interface into services: + +* ``Chill\MainBundle\Security\Resolver\CenterResolverInterface``; +* ``Chill\MainBundle\Security\Resolver\ScopeResolverInterface``; + +Authorization into lists and index pages +======================================== + +Due to the fact that authorization model may be overriden, "list" and "index" pages should not rely on center and scope from controller. This must be delegated to dedicated service, which will be aware of the authorization model. We call them ``ACLAwareRepository``. This service must implements an interface, in order to allow to change the implementation. + +The controller **must not** performs any DQL or SQL query. + +Example in a controller: + +.. code-block:: php + + namespace Chill\TaskBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface; + + + final class SingleTaskController extends AbstractController + { + + private SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository; + + /** + * + * @Route( + * "/{_locale}/task/single-task/list", + * name="chill_task_singletask_list" + * ) + */ + public function listAction( + Request $request + ) { + $this->denyAccessUnlessGranted(TaskVoter::SHOW, null); + + $nb = $this->singleTaskAclAwareRepository->countByAllViewable( + '', // search pattern + [] // search flags + ); + $paginator = $this->paginatorFactory->create($nb); + + if (0 < $nb) { + $tasks = $this->singleTaskAclAwareRepository->findByAllViewable( + '', // search pattern + [] // search flags + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage(), + // ordering: + [ + 'startDate' => 'DESC', + 'endDate' => 'DESC', + ] + ); + } else { + $tasks = []; + } + + return $this->render('@ChillTask/SingleTask/List/index.html.twig', [ + 'tasks' => $tasks, + 'paginator' => $paginator, + 'filter_order' => $filterOrder + ]); + } + } + +Writing ``ACLAwareRepository`` +------------------------------ + +The ACLAwareRepository should rely on interfaces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As described above, the ACLAwareRepository will perform the query for listing entities, and take care of authorization. + +Those "ACLAwareRepositories" must be described into ``interfaces``. + +The service must rely on this interface, and not on the default implementation. + +Example: at first, we design an interface for listing ``SingleTask`` entities: + + +.. code-block:: php + + buildQuery($criterias); + + return $this->addAuthorizations($qb)->select("COUNT(e)")->getQuery()->getResult()->getSingleScalarResult(); + } + + public function findByAuthorized(array $criteria, int $start, int $limit, array $orderBy): array + { + $qb = $this->buildQuery($criterias); + + return $this->getResult($this->addAuthorizations($qb), $start, $limit, $orderBy); + } + + public function getResult(QueryBuilder $qb, int $start, int $limit, array $orderBy): array + { + $qb + ->setFirstResult($start) + ->setMaxResults($limit) + ; + + // add order by logic + + return $qb->getQuery()->getResult(); + } + + public function buildQuery(array $criterias): QueryBuilder + { + $qb = $this->em->createQueryBuilder(); + + // implement you logic with search criteria here + + return $qb; + } + + private function addAuthorizations(QueryBuilder $qb): QueryBuilder + { + // add authorization logic here + return $qb; + } + + } + +Once this logic is executed, it becomes easy to make a new implementation of the repository: + +.. code-block:: php + + namespace Chill\MyOtherBundle\Repository; + + use Doctrine\ORM\EntityManagerInterface; + use Doctrine\ORM\QueryBuilder; + use Chill\MyBundle\Repository\MyEntityACLAwareRepository + + + final class AnotherEntityACLAwareRepository implements MyEntityACLAwareRepositoryInterface { + + private EntityManagerInterface $em; + private \Chill\MyBundle\Repository\MyEntityACLAwareRepository $initial; + + public function __construct( + EntityManagerInterface $em, + \Chill\MyBundle\Repository\MyEntityACLAwareRepository $initial + ) { + $this->em = $em; + $this->initial = $initial; + } + + public function countByAuthorized(array $criterias): int + { + $qb = $this->initial->buildQuery($criterias); + + return $this->addAuthorizations($qb)->select("COUNT(e)")->getQuery()->getResult()->getSingleScalarResult(); + } + + public function findByAuthorized(array $criteria, int $start, int $limit, array $orderBy): array + { + $qb = $this->initial->buildQuery($criterias); + + return $this->initial->getResult($this->addAuthorizations($qb), $start, $limit, $orderBy); + } + + private function addAuthorizations(QueryBuilder $qb): QueryBuilder + { + // add a different authorization logic here + return $qb; + } + + } + +Then, register this service and decorates the old one: + +.. code-block:: yaml + + services: + Chill\MyOtherBundle\Repository\AnotherEntityACLAwareRepository: + autowire: true + autoconfigure: true + decorates: Chill\MyBundle\Repository\MyEntityACLAwareRepositoryInterface: + + + + diff --git a/phpstan-critical.neon b/phpstan-critical.neon index cb0ac38ce..6147f2022 100644 --- a/phpstan-critical.neon +++ b/phpstan-critical.neon @@ -90,51 +90,21 @@ parameters: count: 1 path: src/Bundle/ChillTaskBundle/Controller/TaskController.php - - - message: "#^Undefined variable\\: \\$id$#" - count: 1 - path: src/Bundle/ChillFamilyMembersBundle/Controller/FamilyMemberController.php - - message: "#^Call to an undefined method Chill\\\\MainBundle\\\\CRUD\\\\Controller\\\\AbstractCRUDController\\:\\:getRoleFor\\(\\)\\.$#" count: 1 path: src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php - - - message: "#^Call to an undefined method Chill\\\\MainBundle\\\\Command\\\\ChillImportUsersCommand\\:\\:tempOutput\\(\\)\\.$#" - count: 1 - path: src/Bundle/ChillMainBundle/Command/ChillImportUsersCommand.php - - - - message: "#^Access to an undefined property Chill\\\\MainBundle\\\\Controller\\\\AdminCountryCRUDController\\:\\:\\$paginatorFactory\\.$#" - count: 1 - path: src/Bundle/ChillMainBundle/Controller/AdminCountryCRUDController.php - - message: "#^Call to an undefined method Chill\\\\MainBundle\\\\Controller\\\\UserController\\:\\:createEditForm\\(\\)\\.$#" count: 1 path: src/Bundle/ChillMainBundle/Controller/UserController.php - - - message: "#^Access to an undefined property Chill\\\\MainBundle\\\\Entity\\\\RoleScope\\:\\:\\$new\\.$#" - count: 1 - path: src/Bundle/ChillMainBundle/Entity/RoleScope.php - - message: "#^Undefined variable\\: \\$current$#" count: 1 path: src/Bundle/ChillMainBundle/Pagination/PageGenerator.php - - - message: "#^Access to an undefined property Chill\\\\MainBundle\\\\Routing\\\\MenuComposer\\:\\:\\$routeCollection\\.$#" - count: 1 - path: src/Bundle/ChillMainBundle/Routing/MenuComposer.php - - - - message: "#^Access to an undefined property Chill\\\\MainBundle\\\\Search\\\\SearchApiResult\\:\\:\\$relevance\\.$#" - count: 2 - path: src/Bundle/ChillMainBundle/Search/SearchApiResult.php - - message: "#^Call to an undefined method Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AbstractChillVoter\\:\\:getSupportedAttributes\\(\\)\\.$#" count: 1 @@ -155,53 +125,7 @@ parameters: count: 3 path: src/Bundle/ChillPersonBundle/Controller/PersonController.php - - - message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Controller\\\\TimelinePersonController\\:\\:\\$authorizationHelper\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Controller/TimelinePersonController.php - - - - message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\DataFixtures\\\\ORM\\\\LoadHousehold\\:\\:\\$personIds\\.$#" - count: 2 - path: src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHousehold.php - - - - message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Form\\\\CreationPersonType\\:\\:\\$centerTransformer\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Form/CreationPersonType.php - - - - message: "#^Access to an undefined property Chill\\\\ReportBundle\\\\Timeline\\\\TimelineReportProvider\\:\\:\\$security\\.$#" - count: 4 - path: src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php - - - - message: "#^Access to an undefined property Chill\\\\AsideActivityBundle\\\\Entity\\\\AsideActivityCategory\\:\\:\\$oldParent\\.$#" - count: 2 - path: src/Bundle/ChillAsideActivityBundle/src/Entity/AsideActivityCategory.php - - - - message: "#^Access to an undefined property Chill\\\\AsideActivityBundle\\\\Form\\\\AsideActivityCategoryType\\:\\:\\$categoryRender\\.$#" - count: 2 - path: src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityCategoryType.php - - - - message: "#^Access to an undefined property Chill\\\\AsideActivityBundle\\\\Form\\\\AsideActivityFormType\\:\\:\\$translatableStringHelper\\.$#" - count: 1 - path: src/Bundle/ChillAsideActivityBundle/src/Form/AsideActivityFormType.php - - - - message: "#^Access to an undefined property Chill\\\\CalendarBundle\\\\DataFixtures\\\\ORM\\\\LoadCalendarRange\\:\\:\\$userRepository\\.$#" - count: 2 - path: src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCalendarRange.php - - - - message: "#^Access to an undefined property Chill\\\\CustomFieldsBundle\\\\Form\\\\DataTransformer\\\\JsonCustomFieldToArrayTransformer\\:\\:\\$customField\\.$#" - count: 3 - path: src/Bundle/ChillCustomFieldsBundle/Form/DataTransformer/JsonCustomFieldToArrayTransformer.php - - message: "#^Call to an undefined method Chill\\\\ThirdPartyBundle\\\\Form\\\\Type\\\\PickThirdPartyTypeCategoryType\\:\\:transform\\(\\)\\.$#" count: 1 path: src/Bundle/ChillThirdPartyBundle/Form/Type/PickThirdPartyTypeCategoryType.php - diff --git a/phpstan-types.neon b/phpstan-types.neon index f10574600..dca84e17f 100644 --- a/phpstan-types.neon +++ b/phpstan-types.neon @@ -705,11 +705,6 @@ parameters: count: 1 path: src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php - - - message: "#^Method Chill\\\\PersonBundle\\\\Search\\\\SimilarityPersonSearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#" - count: 1 - path: src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" count: 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 46b6698ec..7943ab3aa 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,6 +4,7 @@ parameters: - src/ excludePaths: - src/Bundle/*/Tests/* + - src/Bundle/*/tests/* - src/Bundle/*/Test/* - src/Bundle/*/config/* - src/Bundle/*/migrations/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3faa756b3..80367c691 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -37,6 +37,9 @@ src/Bundle/ChillCalendarBundle/Tests/ + + src/Bundle/ChillDocGeneratorBundle/tests/ + diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 36dd609f3..3f8356bb4 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -4,15 +4,23 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Controller; +use Chill\ActivityBundle\Entity\ActivityReason; use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; +use Chill\ActivityBundle\Repository\ActivityRepository; +use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository; +use Chill\ActivityBundle\Repository\ActivityTypeRepository; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Repository\LocationRepository; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Privacy\PrivacyEvent; +use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; +use Chill\PersonBundle\Repository\PersonRepository; +use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; +use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -25,26 +33,54 @@ use Symfony\Component\Serializer\SerializerInterface; final class ActivityController extends AbstractController { - protected EventDispatcherInterface $eventDispatcher; + private EventDispatcherInterface $eventDispatcher; - protected AuthorizationHelper $authorizationHelper; + private LoggerInterface $logger; - protected LoggerInterface $logger; + private SerializerInterface $serializer; - protected SerializerInterface $serializer; + private ActivityACLAwareRepositoryInterface $activityACLAwareRepository; - protected ActivityACLAwareRepositoryInterface $activityACLAwareRepository; + private ActivityTypeRepository $activityTypeRepository; + + private ThirdPartyRepository $thirdPartyRepository; + + private PersonRepository $personRepository; + + private LocationRepository $locationRepository; + + private EntityManagerInterface $entityManager; + + private ActivityRepository $activityRepository; + + private AccompanyingPeriodRepository $accompanyingPeriodRepository; + + private ActivityTypeCategoryRepository $activityTypeCategoryRepository; public function __construct( ActivityACLAwareRepositoryInterface $activityACLAwareRepository, + ActivityTypeRepository $activityTypeRepository, + ActivityTypeCategoryRepository $activityTypeCategoryRepository, + PersonRepository $personRepository, + ThirdPartyRepository $thirdPartyRepository, + LocationRepository $locationRepository, + ActivityRepository $activityRepository, + AccompanyingPeriodRepository $accompanyingPeriodRepository, + EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, - AuthorizationHelper $authorizationHelper, LoggerInterface $logger, SerializerInterface $serializer ) { $this->activityACLAwareRepository = $activityACLAwareRepository; + $this->activityTypeRepository = $activityTypeRepository; + $this->activityTypeCategoryRepository = $activityTypeCategoryRepository; + $this->personRepository = $personRepository; + $this->thirdPartyRepository = $thirdPartyRepository; + $this->locationRepository = $locationRepository; + $this->activityRepository = $activityRepository; + $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; + $this->entityManager = $entityManager; $this->eventDispatcher = $eventDispatcher; - $this->authorizationHelper = $authorizationHelper; $this->logger = $logger; $this->serializer = $serializer; } @@ -65,10 +101,10 @@ final class ActivityController extends AbstractController $activities = $this->activityACLAwareRepository ->findByPerson($person, ActivityVoter::SEE, 0, null); - $event = new PrivacyEvent($person, array( + $event = new PrivacyEvent($person, [ 'element_class' => Activity::class, 'action' => 'list' - )); + ]); $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); $view = 'ChillActivityBundle:Activity:listPerson.html.twig'; @@ -93,7 +129,6 @@ final class ActivityController extends AbstractController public function selectTypeAction(Request $request): Response { - $em = $this->getDoctrine()->getManager(); $view = null; [$person, $accompanyingPeriod] = $this->getEntity($request); @@ -106,12 +141,17 @@ final class ActivityController extends AbstractController $data = []; - $activityTypeCategories = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityTypeCategory::class) + $activityTypeCategories = $this + ->activityTypeCategoryRepository ->findBy(['active' => true], ['ordering' => 'ASC']); foreach ($activityTypeCategories as $activityTypeCategory) { - $activityTypes = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityType::class) - ->findBy(['active' => true, 'category' => $activityTypeCategory], ['ordering' => 'ASC']); + $activityTypes = $this + ->activityTypeRepository + ->findBy( + ['active' => true, 'category' => $activityTypeCategory], + ['ordering' => 'ASC'] + ); $data[] = [ 'activityTypeCategory' => $activityTypeCategory, @@ -119,12 +159,6 @@ final class ActivityController extends AbstractController ]; } - if ($request->query->has('activityData')) { - $activityData = $request->query->get('activityData'); - } else { - $activityData = []; - } - if ($view === null) { throw $this->createNotFoundException('Template not found'); } @@ -133,14 +167,13 @@ final class ActivityController extends AbstractController 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, 'data' => $data, - 'activityData' => $activityData + 'activityData' => $request->query->get('activityData', []), ]); } public function newAction(Request $request): Response { $view = null; - $em = $this->getDoctrine()->getManager(); [$person, $accompanyingPeriod] = $this->getEntity($request); @@ -151,8 +184,7 @@ final class ActivityController extends AbstractController } $activityType_id = $request->get('activityType_id', 0); - $activityType = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityType::class) - ->find($activityType_id); + $activityType = $this->activityTypeRepository->find($activityType_id); if (isset($activityType) && !$activityType->isActive()) { throw new \InvalidArgumentException('Activity type must be active'); @@ -210,20 +242,20 @@ final class ActivityController extends AbstractController if (array_key_exists('personsId', $activityData)) { foreach($activityData['personsId'] as $personId){ - $concernedPerson = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)->find($personId); + $concernedPerson = $this->personRepository->find($personId); $entity->addPerson($concernedPerson); } } if (array_key_exists('professionalsId', $activityData)) { foreach($activityData['professionalsId'] as $professionalsId){ - $professional = $em->getRepository(\Chill\ThirdPartyBundle\Entity\ThirdParty::class)->find($professionalsId); + $professional = $this->thirdPartyRepository->find($professionalsId); $entity->addThirdParty($professional); } } if (array_key_exists('location', $activityData)) { - $location = $em->getRepository(\Chill\MainBundle\Entity\Location::class)->find($activityData['location']); + $location = $this->locationRepository->find($activityData['location']); $entity->setLocation($location); } @@ -248,8 +280,8 @@ final class ActivityController extends AbstractController ])->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $em->persist($entity); - $em->flush(); + $this->entityManager->persist($entity); + $this->entityManager->flush(); $this->addFlash('success', $this->get('translator')->trans('Success : activity created!')); @@ -281,7 +313,6 @@ final class ActivityController extends AbstractController public function showAction(Request $request, int $id): Response { $view = null; - $em = $this->getDoctrine()->getManager(); [$person, $accompanyingPeriod] = $this->getEntity($request); @@ -291,13 +322,14 @@ final class ActivityController extends AbstractController $view = 'ChillActivityBundle:Activity:showPerson.html.twig'; } - $entity = $em->getRepository('ChillActivityBundle:Activity')->find($id); + $entity = $this->activityRepository->find($id); - if (!$entity) { + if (null === $entity) { throw $this->createNotFoundException('Unable to find Activity entity.'); } if (null !== $accompanyingPeriod) { + // @TODO: Properties created dynamically. $entity->personsAssociated = $entity->getPersonsAssociated(); $entity->personsNotAssociated = $entity->getPersonsNotAssociated(); } @@ -305,7 +337,7 @@ final class ActivityController extends AbstractController // TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période // $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity); - $deleteForm = $this->createDeleteForm($id, $person, $accompanyingPeriod); + $deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod); // TODO /* @@ -321,22 +353,20 @@ final class ActivityController extends AbstractController throw $this->createNotFoundException('Template not found'); } - return $this->render($view, array( + return $this->render($view, [ 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, 'entity' => $entity, 'delete_form' => $deleteForm->createView(), - )); + ]); } /** * Displays a form to edit an existing Activity entity. - * */ public function editAction(int $id, Request $request): Response { $view = null; - $em = $this->getDoctrine()->getManager(); [$person, $accompanyingPeriod] = $this->getEntity($request); @@ -346,9 +376,9 @@ final class ActivityController extends AbstractController $view = 'ChillActivityBundle:Activity:editPerson.html.twig'; } - $entity = $em->getRepository('ChillActivityBundle:Activity')->find($id); + $entity = $this->activityRepository->find($id); - if (!$entity) { + if (null === $entity) { throw $this->createNotFoundException('Unable to find Activity entity.'); } @@ -363,17 +393,18 @@ final class ActivityController extends AbstractController ])->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $em->persist($entity); - $em->flush(); + $this->entityManager->persist($entity); + $this->entityManager->flush(); $this->addFlash('success', $this->get('translator')->trans('Success : activity updated!')); $params = $this->buildParamsToUrl($person, $accompanyingPeriod); - $params['id'] = $id; + $params['id'] = $entity->getId(); + return $this->redirectToRoute('chill_activity_activity_show', $params); } - $deleteForm = $this->createDeleteForm($id, $person, $accompanyingPeriod); + $deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod); /* * TODO @@ -391,24 +422,22 @@ final class ActivityController extends AbstractController $activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']); - return $this->render($view, array( + return $this->render($view, [ 'entity' => $entity, 'edit_form' => $form->createView(), 'delete_form' => $deleteForm->createView(), 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, 'activity_json' => $activity_array - )); + ]); } /** * Deletes a Activity entity. - * */ public function deleteAction(Request $request, $id) { $view = null; - $em = $this->getDoctrine()->getManager(); [$person, $accompanyingPeriod] = $this->getEntity($request); @@ -418,8 +447,7 @@ final class ActivityController extends AbstractController $view = 'ChillActivityBundle:Activity:confirm_deletePerson.html.twig'; } - /* @var $activity Activity */ - $activity = $em->getRepository('ChillActivityBundle:Activity')->find($id); + $activity = $this->activityRepository->find($id); if (!$activity) { throw $this->createNotFoundException('Unable to find Activity entity.'); @@ -428,35 +456,37 @@ final class ActivityController extends AbstractController // TODO // $this->denyAccessUnlessGranted('CHILL_ACTIVITY_DELETE', $activity); - $form = $this->createDeleteForm($id, $person, $accompanyingPeriod); + $form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod); if ($request->getMethod() === Request::METHOD_DELETE) { $form->handleRequest($request); if ($form->isValid()) { - - $this->logger->notice("An activity has been removed", array( + $this->logger->notice("An activity has been removed", [ 'by_user' => $this->getUser()->getUsername(), 'activity_id' => $activity->getId(), 'person_id' => $activity->getPerson() ? $activity->getPerson()->getId() : null, 'comment' => $activity->getComment()->getComment(), 'scope_id' => $activity->getScope() ? $activity->getScope()->getId() : null, 'reasons_ids' => $activity->getReasons() - ->map(function ($ar) { return $ar->getId(); }) + ->map( + static fn (ActivityReason $ar): int => $ar->getId() + ) ->toArray(), 'type_id' => $activity->getType()->getId(), 'duration' => $activity->getDurationTime() ? $activity->getDurationTime()->format('U') : null, 'date' => $activity->getDate()->format('Y-m-d'), 'attendee' => $activity->getAttendee() - )); + ]); - $em->remove($activity); - $em->flush(); + $this->entityManager->remove($activity); + $this->entityManager->flush(); $this->addFlash('success', $this->get('translator') ->trans("The activity has been successfully removed.")); $params = $this->buildParamsToUrl($person, $accompanyingPeriod); + return $this->redirectToRoute('chill_activity_activity_list', $params); } } @@ -465,18 +495,18 @@ final class ActivityController extends AbstractController throw $this->createNotFoundException('Template not found'); } - return $this->render($view, array( + return $this->render($view, [ 'activity' => $activity, 'delete_form' => $form->createView(), 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, - )); + ]); } /** * Creates a form to delete a Activity entity by id. */ - private function createDeleteForm(int $id, ?Person $person, ?AccompanyingPeriod $accompanyingPeriod): Form + private function createDeleteForm(int $id, ?Person $person, ?AccompanyingPeriod $accompanyingPeriod): FormInterface { $params = $this->buildParamsToUrl($person, $accompanyingPeriod); $params['id'] = $id; @@ -484,19 +514,17 @@ final class ActivityController extends AbstractController return $this->createFormBuilder() ->setAction($this->generateUrl('chill_activity_activity_delete', $params)) ->setMethod('DELETE') - ->add('submit', SubmitType::class, array('label' => 'Delete')) - ->getForm() - ; + ->add('submit', SubmitType::class, ['label' => 'Delete']) + ->getForm(); } private function getEntity(Request $request): array { - $em = $this->getDoctrine()->getManager(); $person = $accompanyingPeriod = null; if ($request->query->has('person_id')) { $person_id = $request->get('person_id'); - $person = $em->getRepository(Person::class)->find($person_id); + $person = $this->personRepository->find($person_id); if ($person === null) { throw $this->createNotFoundException('Person not found'); @@ -505,7 +533,7 @@ final class ActivityController extends AbstractController $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); } elseif ($request->query->has('accompanying_period_id')) { $accompanying_period_id = $request->get('accompanying_period_id'); - $accompanyingPeriod = $em->getRepository(AccompanyingPeriod::class)->find($accompanying_period_id); + $accompanyingPeriod = $this->accompanyingPeriodRepository->find($accompanying_period_id); if ($accompanyingPeriod === null) { throw $this->createNotFoundException('Accompanying Period not found'); @@ -518,21 +546,20 @@ final class ActivityController extends AbstractController } return [ - $person, $accompanyingPeriod + $person, + $accompanyingPeriod ]; } - private function buildParamsToUrl( - ?Person $person, - ?AccompanyingPeriod $accompanyingPeriod - ): array { + private function buildParamsToUrl(?Person $person, ?AccompanyingPeriod $accompanyingPeriod): array + { $params = []; - if ($person) { + if (null !== $person) { $params['person_id'] = $person->getId(); } - if ($accompanyingPeriod) { + if (null !== $accompanyingPeriod) { $params['accompanying_period_id'] = $accompanyingPeriod->getId(); } diff --git a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php index 18a6f83d3..015900043 100644 --- a/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php +++ b/src/Bundle/ChillActivityBundle/DataFixtures/ORM/LoadActivity.php @@ -36,6 +36,8 @@ use Chill\MainBundle\DataFixtures\ORM\LoadScopes; class LoadActivity extends AbstractFixture implements OrderedFixtureInterface { + use \Symfony\Component\DependencyInjection\ContainerAwareTrait; + /** * @var \Faker\Generator */ @@ -80,15 +82,10 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface * * @return \Chill\ActivityBundle\Entity\ActivityReason */ - private function getRandomActivityReason(array $excludingIds) + private function getRandomActivityReason() { $reasonRef = LoadActivityReason::$references[array_rand(LoadActivityReason::$references)]; - if (in_array($this->getReference($reasonRef)->getId(), $excludingIds, true)) { - // we have a reason which should be excluded. Find another... - return $this->getRandomActivityReason($excludingIds); - } - return $this->getReference($reasonRef); } @@ -103,7 +100,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface return $this->getReference($userRef); } - public function newRandomActivity($person) + public function newRandomActivity($person): ?Activity { $activity = (new Activity()) ->setUser($this->getRandomUser()) @@ -116,11 +113,13 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface // ->setAttendee($this->faker->boolean()) - $usedId = array(); for ($i = 0; $i < rand(0, 4); $i++) { - $reason = $this->getRandomActivityReason($usedId); - $usedId[] = $reason->getId(); - $activity->addReason($reason); + $reason = $this->getRandomActivityReason(); + if (null !== $reason) { + $activity->addReason($reason); + } else { + return null; + } } return $activity; @@ -137,7 +136,9 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface for ($i = 0; $i < $activityNbr; $i ++) { $activity = $this->newRandomActivity($person); - $manager->persist($activity); + if (null !== $activity) { + $manager->persist($activity); + } } } $manager->flush(); diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index e25448f10..4ed654c85 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -1,27 +1,8 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - namespace Chill\ActivityBundle\Entity; use Chill\DocStoreBundle\Entity\Document; -use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Location; use Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodLinkedWithSocialIssuesEntityInterface; @@ -38,7 +19,7 @@ use Chill\MainBundle\Entity\HasCenterInterface; use Chill\MainBundle\Entity\HasScopeInterface; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; -use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; @@ -202,7 +183,7 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer return $this->id; } - public function setUser(User $user): self + public function setUser(UserInterface $user): self { $this->user = $user; diff --git a/src/Bundle/ChillActivityBundle/Entity/ActivityTypeCategory.php b/src/Bundle/ChillActivityBundle/Entity/ActivityTypeCategory.php index a95424312..4b06ca9b5 100644 --- a/src/Bundle/ChillActivityBundle/Entity/ActivityTypeCategory.php +++ b/src/Bundle/ChillActivityBundle/Entity/ActivityTypeCategory.php @@ -1,31 +1,12 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** - * Class ActivityTypeCateogry - * - * @package Chill\ActivityBundle\Entity * @ORM\Entity() * @ORM\Table(name="activitytypecategory") * @ORM\HasLifecycleCallbacks() @@ -37,7 +18,7 @@ class ActivityTypeCategory * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ - private ?int $id; + private ?int $id = null; /** * @ORM\Column(type="json") @@ -54,10 +35,7 @@ class ActivityTypeCategory */ private float $ordering = 0.0; - /** - * Get id - */ - public function getId(): int + public function getId(): ?int { return $this->id; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php index 6614caace..3e5cab378 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php @@ -1,70 +1,39 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Aggregator; +use Chill\ActivityBundle\Repository\ActivityReasonCategoryRepository; +use Chill\ActivityBundle\Repository\ActivityReasonRepository; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Symfony\Component\Form\FormBuilderInterface; use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Export\AggregatorInterface; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityRepository; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Chill\MainBundle\Export\ExportElementValidatedInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -/** - * - * - * @author Julien Fastré - */ -class ActivityReasonAggregator implements AggregatorInterface, - ExportElementValidatedInterface +class ActivityReasonAggregator implements AggregatorInterface, ExportElementValidatedInterface { - /** - * - * @var EntityRepository - */ - protected $categoryRepository; + protected ActivityReasonCategoryRepository $activityReasonCategoryRepository; - /** - * - * @var EntityRepository - */ - protected $reasonRepository; + protected ActivityReasonRepository $activityReasonRepository; - /** - * - * @var TranslatableStringHelper - */ - protected $stringHelper; + protected TranslatableStringHelperInterface $translatableStringHelper; public function __construct( - EntityRepository $categoryRepository, - EntityRepository $reasonRepository, - TranslatableStringHelper $stringHelper + ActivityReasonCategoryRepository $activityReasonCategoryRepository, + ActivityReasonRepository $activityReasonRepository, + TranslatableStringHelper $translatableStringHelper ) { - $this->categoryRepository = $categoryRepository; - $this->reasonRepository = $reasonRepository; - $this->stringHelper = $stringHelper; + $this->activityReasonCategoryRepository = $activityReasonCategoryRepository; + $this->activityReasonRepository = $activityReasonRepository; + $this->translatableStringHelper = $translatableStringHelper; } public function alterQuery(QueryBuilder $qb, $data) @@ -77,7 +46,7 @@ class ActivityReasonAggregator implements AggregatorInterface, $elem = 'category.id'; $alias = 'activity_categories_id'; } else { - throw new \RuntimeException('the data provided are not recognized'); + throw new \RuntimeException('The data provided are not recognized.'); } $qb->addSelect($elem.' as '.$alias); @@ -93,11 +62,12 @@ class ActivityReasonAggregator implements AggregatorInterface, (! array_key_exists('activity', $join)) ) { $qb->add( - 'join', - array('activity' => - new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons') - ), - true); + 'join', + [ + 'activity' => new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons') + ], + true + ); } // join category if necessary @@ -143,28 +113,33 @@ class ActivityReasonAggregator implements AggregatorInterface, public function buildForm(FormBuilderInterface $builder) { - $builder->add('level', ChoiceType::class, array( - 'choices' => array( - 'By reason' => 'reasons', - 'By category of reason' => 'categories' - ), - 'multiple' => false, - 'expanded' => true, - 'label' => 'Reason\'s level' - )); + $builder->add( + 'level', + ChoiceType::class, + [ + 'choices' => [ + 'By reason' => 'reasons', + 'By category of reason' => 'categories' + ], + 'multiple' => false, + 'expanded' => true, + 'label' => "Reason's level" + ] + ); } public function validateForm($data, ExecutionContextInterface $context) { if ($data['level'] === null) { - $context->buildViolation("The reasons's level should not be empty") + $context + ->buildViolation("The reasons's level should not be empty.") ->addViolation(); } } - public function getTitle() + public function getTitle() { - return "Aggregate by activity reason"; + return 'Aggregate by activity reason'; } public function addRole() @@ -177,41 +152,33 @@ class ActivityReasonAggregator implements AggregatorInterface, // for performance reason, we load data from db only once switch ($data['level']) { case 'reasons': - $this->reasonRepository->findBy(array('id' => $values)); + $this->activityReasonRepository->findBy(['id' => $values]); break; case 'categories': - $this->categoryRepository->findBy(array('id' => $values)); + $this->activityReasonCategoryRepository->findBy(['id' => $values]); break; default: - throw new \RuntimeException(sprintf("the level data '%s' is invalid", - $data['level'])); + throw new \RuntimeException(sprintf("The level data '%s' is invalid.", $data['level'])); } return function($value) use ($data) { if ($value === '_header') { - return $data['level'] === 'reasons' ? - 'Group by reasons' - : - 'Group by categories of reason' - ; + return $data['level'] === 'reasons' ? 'Group by reasons' : 'Group by categories of reason'; } switch ($data['level']) { case 'reasons': - /* @var $r \Chill\ActivityBundle\Entity\ActivityReason */ - $r = $this->reasonRepository->find($value); + $r = $this->activityReasonRepository->find($value); - return $this->stringHelper->localize($r->getCategory()->getName()) - ." > " - . $this->stringHelper->localize($r->getName()); - ; - break; + return sprintf( + "%s > %s", + $this->translatableStringHelper->localize($r->getCategory()->getName()), + $this->translatableStringHelper->localize($r->getName()) + ); case 'categories': - $c = $this->categoryRepository->find($value); + $c = $this->activityReasonCategoryRepository->find($value); - return $this->stringHelper->localize($c->getName()); - break; - // no need for a default : the default was already set above + return $this->translatableStringHelper->localize($c->getName()); } }; @@ -222,12 +189,14 @@ class ActivityReasonAggregator implements AggregatorInterface, // add select element if ($data['level'] === 'reasons') { return array('activity_reasons_id'); - } elseif ($data['level'] === 'categories') { - return array ('activity_categories_id'); - } else { - throw new \RuntimeException('the data provided are not recognised'); } + if ($data['level'] === 'categories') { + return array ('activity_categories_id'); + } + + throw new \RuntimeException('The data provided are not recognised.'); + } } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php index a0e6d3966..bd90e8eb6 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php @@ -1,61 +1,32 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Aggregator; +use Chill\ActivityBundle\Repository\ActivityTypeRepository; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Symfony\Component\Form\FormBuilderInterface; use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Export\AggregatorInterface; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityRepository; -use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; -/** - * - * - * @author Julien Fastré - */ class ActivityTypeAggregator implements AggregatorInterface { + protected ActivityTypeRepository $activityTypeRepository; - /** - * - * @var EntityRepository - */ - protected $typeRepository; + protected TranslatableStringHelperInterface $translatableStringHelper; - /** - * - * @var TranslatableStringHelper - */ - protected $stringHelper; - - const KEY = 'activity_type_aggregator'; + public const KEY = 'activity_type_aggregator'; public function __construct( - EntityRepository $typeRepository, - TranslatableStringHelper $stringHelper + ActivityTypeRepository $activityTypeRepository, + TranslatableStringHelperInterface $translatableStringHelper ) { - $this->typeRepository = $typeRepository; - $this->stringHelper = $stringHelper; + $this->activityTypeRepository = $activityTypeRepository; + $this->translatableStringHelper = $translatableStringHelper; } public function alterQuery(QueryBuilder $qb, $data) @@ -64,7 +35,7 @@ class ActivityTypeAggregator implements AggregatorInterface $qb->addSelect(sprintf('IDENTITY(activity.type) AS %s', self::KEY)); // add the "group by" part - $groupBy = $qb->addGroupBy(self::KEY); + $qb->addGroupBy(self::KEY); } /** @@ -97,7 +68,7 @@ class ActivityTypeAggregator implements AggregatorInterface public function getTitle() { - return "Aggregate by activity type"; + return 'Aggregate by activity type'; } public function addRole() @@ -108,17 +79,16 @@ class ActivityTypeAggregator implements AggregatorInterface public function getLabels($key, array $values, $data): \Closure { // for performance reason, we load data from db only once - $this->typeRepository->findBy(array('id' => $values)); + $this->activityTypeRepository->findBy(['id' => $values]); return function($value): string { if ($value === '_header') { return 'Activity type'; } - /* @var $r \Chill\ActivityBundle\Entity\ActivityType */ - $t = $this->typeRepository->find($value); + $t = $this->activityTypeRepository->find($value); - return $this->stringHelper->localize($t->getName()); + return $this->translatableStringHelper->localize($t->getName()); }; } diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php index ca01e0ae5..8e4635ef2 100644 --- a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php @@ -1,51 +1,26 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ + namespace Chill\ActivityBundle\Export\Aggregator; +use Chill\MainBundle\Repository\UserRepository; use Symfony\Component\Form\FormBuilderInterface; use Doctrine\ORM\QueryBuilder; use Chill\MainBundle\Export\AggregatorInterface; use Symfony\Component\Security\Core\Role\Role; -use Doctrine\ORM\Query\Expr\Join; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityManagerInterface; -use Chill\MainBundle\Entity\User; -/** - * - * - * @author Julien Fastré - */ class ActivityUserAggregator implements AggregatorInterface { - /** - * - * @var EntityManagerInterface - */ - protected $em; - - const KEY = 'activity_user_id'; - - function __construct(EntityManagerInterface $em) - { - $this->em = $em; + public const KEY = 'activity_user_id'; + + private UserRepository $userRepository; + + public function __construct( + UserRepository $userRepository + ) { + $this->userRepository = $userRepository; } - + public function addRole() { return new Role(ActivityStatsVoter::STATS); @@ -53,9 +28,9 @@ class ActivityUserAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { - // add select element + // add select element $qb->addSelect(sprintf('IDENTITY(activity.user) AS %s', self::KEY)); - + // add the "group by" part $qb->addGroupBy(self::KEY); } @@ -73,17 +48,14 @@ class ActivityUserAggregator implements AggregatorInterface public function getLabels($key, $values, $data): \Closure { // preload users at once - $this->em->getRepository(User::class) - ->findBy(['id' => $values]); - + $this->userRepository->findBy(['id' => $values]); + return function($value) { - switch ($value) { - case '_header': - return 'activity user'; - default: - return $this->em->getRepository(User::class)->find($value) - ->getUsername(); + if ($value === '_header') { + return 'activity user'; } + + return $this->userRepository->find($value)->getUsername(); }; } @@ -94,6 +66,6 @@ class ActivityUserAggregator implements AggregatorInterface public function getTitle(): string { - return "Aggregate by activity user"; + return 'Aggregate by activity user'; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Export/CountActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/CountActivity.php index 3498d7a13..3d5d798f8 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/CountActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/CountActivity.php @@ -1,64 +1,40 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Export; +use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\MainBundle\Export\ExportInterface; -use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Export\FormatterInterface; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Role\Role; use Doctrine\ORM\Query; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityManagerInterface; -/** - * - * - * @author Julien Fastré - */ class CountActivity implements ExportInterface { - /** - * - * @var EntityManagerInterface - */ - protected $entityManager; - + protected ActivityRepository $activityRepository; + public function __construct( - EntityManagerInterface $em - ) - { - $this->entityManager = $em; + ActivityRepository $activityRepository + ) { + $this->activityRepository = $activityRepository; } - - public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + + public function buildForm(FormBuilderInterface $builder) { - + } public function getDescription() { - return "Count activities by various parameters."; + return 'Count activities by various parameters.'; } public function getTitle() { - return "Count activities"; + return 'Count activities'; } public function getType() @@ -68,26 +44,26 @@ class CountActivity implements ExportInterface public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) { - $qb = $this->entityManager->createQueryBuilder(); - $centers = array_map(function($el) { return $el['center']; }, $acl); - - $qb->select('COUNT(activity.id) as export_count_activity') - ->from('ChillActivityBundle:Activity', 'activity') - ->join('activity.person', 'person') - ; - - $qb->where($qb->expr()->in('person.center', ':centers')) - ->setParameter('centers', $centers) - ; - + $centers = array_map(static fn($el) => $el['center'], $acl); + + $qb = $this + ->activityRepository + ->createQueryBuilder('activity') + ->select('COUNT(activity.id) as export_count_activity') + ->join('activity.person', 'person'); + + $qb + ->where($qb->expr()->in('person.center', ':centers')) + ->setParameter('centers', $centers); + return $qb; } - + public function supportsModifiers() { - return array('person', 'activity'); + return ['person', 'activity']; } - + public function requiredRole() { return new Role(ActivityStatsVoter::STATS); @@ -95,7 +71,7 @@ class CountActivity implements ExportInterface public function getAllowedFormattersTypes() { - return array(\Chill\MainBundle\Export\FormatterInterface::TYPE_TABULAR); + return [FormatterInterface::TYPE_TABULAR]; } public function getLabels($key, array $values, $data) @@ -103,19 +79,13 @@ class CountActivity implements ExportInterface if ($key !== 'export_count_activity') { throw new \LogicException("the key $key is not used by this export"); } - - return function($value) { - return $value === '_header' ? - 'Number of activities' - : - $value - ; - }; + + return static fn($value) => $value === '_header' ? 'Number of activities' : $value; } public function getQueryKeys($data) { - return array('export_count_activity'); + return ['export_count_activity']; } public function getResult($qb, $data) diff --git a/src/Bundle/ChillActivityBundle/Export/Export/ListActivity.php b/src/Bundle/ChillActivityBundle/Export/Export/ListActivity.php index 6e5d7b0b8..12338e225 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/ListActivity.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/ListActivity.php @@ -1,31 +1,14 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Export; +use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\MainBundle\Export\ListInterface; use Chill\ActivityBundle\Entity\ActivityReason; -use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Entity\Scope; -use Chill\ActivityBundle\Entity\ActivityType; -use Doctrine\ORM\Query\Expr; -use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use Doctrine\DBAL\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; use Symfony\Component\Form\FormBuilderInterface; @@ -37,33 +20,17 @@ use Chill\MainBundle\Export\FormatterInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Validator\Context\ExecutionContextInterface; -/** - * Create a list for all activities - * - * @author Julien Fastré - */ class ListActivity implements ListInterface { + private ActivityRepository $activityRepository; - /** - * - * @var EntityManagerInterface - */ - protected $entityManager; + protected EntityManagerInterface $entityManager; - /** - * - * @var TranslatorInterface - */ - protected $translator; + protected TranslatorInterface $translator; - /** - * - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; + protected TranslatableStringHelperInterface $translatableStringHelper; - protected $fields = array( + protected array $fields = [ 'id', 'date', 'durationTime', @@ -75,32 +42,28 @@ class ListActivity implements ListInterface 'person_firstname', 'person_lastname', 'person_id' - ); + ]; public function __construct( - EntityManagerInterface $em, - TranslatorInterface $translator, - TranslatableStringHelper $translatableStringHelper - ) - { + EntityManagerInterface $em, + TranslatorInterface $translator, + TranslatableStringHelperInterface $translatableStringHelper, + ActivityRepository $activityRepository + ) { $this->entityManager = $em; $this->translator = $translator; $this->translatableStringHelper = $translatableStringHelper; + $this->activityRepository = $activityRepository; } - /** - * {@inheritDoc} - * - * @param FormBuilderInterface $builder - */ public function buildForm(FormBuilderInterface $builder) { - $builder->add('fields', ChoiceType::class, array( + $builder->add('fields', ChoiceType::class, [ 'multiple' => true, 'expanded' => true, 'choices' => array_combine($this->fields, $this->fields), 'label' => 'Fields to include in export', - 'constraints' => [new Callback(array( + 'constraints' => [new Callback([ 'callback' => function($selected, ExecutionContextInterface $context) { if (count($selected) === 0) { $context->buildViolation('You must select at least one element') @@ -108,19 +71,14 @@ class ListActivity implements ListInterface ->addViolation(); } } - ))] - )); + ])] + ]); } - /** - * {@inheritDoc} - * - * @return type - */ public function getAllowedFormattersTypes() { - return array(FormatterInterface::TYPE_LIST); + return [FormatterInterface::TYPE_LIST]; } public function getDescription() @@ -133,29 +91,32 @@ class ListActivity implements ListInterface switch ($key) { case 'date' : - return function($value) { - if ($value === '_header') return 'date'; + return static function($value) { + if ($value === '_header') { + return 'date'; + } $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value); return $date->format('d-m-Y'); }; case 'attendee': - return function($value) { - if ($value === '_header') return 'attendee'; + return static function($value) { + if ($value === '_header') { + return 'attendee'; + } return $value ? 1 : 0; }; case 'list_reasons' : - /* @var $activityReasonsRepository EntityRepository */ - $activityRepository = $this->entityManager - ->getRepository('ChillActivityBundle:Activity'); + $activityRepository = $this->activityRepository; - return function($value) use ($activityRepository) { - if ($value === '_header') return 'activity reasons'; + return function($value) use ($activityRepository): string { + if ($value === '_header') { + return 'activity reasons'; + } - $activity = $activityRepository - ->find($value); + $activity = $activityRepository->find($value); return implode(", ", array_map(function(ActivityReason $r) { @@ -168,21 +129,25 @@ class ListActivity implements ListInterface }; case 'circle_name' : return function($value) { - if ($value === '_header') return 'circle'; + if ($value === '_header') { + return 'circle'; + } - return $this->translatableStringHelper - ->localize(json_decode($value, true)); + return $this->translatableStringHelper->localize(json_decode($value, true)); }; case 'type_name' : return function($value) { - if ($value === '_header') return 'activity type'; + if ($value === '_header') { + return 'activity type'; + } - return $this->translatableStringHelper - ->localize(json_decode($value, true)); + return $this->translatableStringHelper->localize(json_decode($value, true)); }; default: - return function($value) use ($key) { - if ($value === '_header') return $key; + return static function($value) use ($key) { + if ($value === '_header') { + return $key; + } return $value; }; @@ -209,14 +174,13 @@ class ListActivity implements ListInterface return 'activity'; } - public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) { $centers = array_map(function($el) { return $el['center']; }, $acl); // throw an error if any fields are present if (!\array_key_exists('fields', $data)) { - throw new \Doctrine\DBAL\Exception\InvalidArgumentException("any fields " - . "have been checked"); + throw new InvalidArgumentException('Any fields have been checked.'); } $qb = $this->entityManager->createQueryBuilder(); @@ -227,7 +191,6 @@ class ListActivity implements ListInterface ->join('person.center', 'center') ->andWhere('center IN (:authorized_centers)') ->setParameter('authorized_centers', $centers); - ; foreach ($this->fields as $f) { if (in_array($f, $data['fields'])) { @@ -269,8 +232,6 @@ class ListActivity implements ListInterface } } - - return $qb; } @@ -281,7 +242,7 @@ class ListActivity implements ListInterface public function supportsModifiers() { - return array('activity', 'person'); + return ['activity', 'person']; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php b/src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php index 0d48130b6..f22623d9f 100644 --- a/src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php +++ b/src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php @@ -4,12 +4,13 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Export\Export; +use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\MainBundle\Export\ExportInterface; -use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Export\FormatterInterface; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Role\Role; use Doctrine\ORM\Query; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityManagerInterface; /** * This export allow to compute stats on activity duration. @@ -18,7 +19,7 @@ use Doctrine\ORM\EntityManagerInterface; */ class StatActivityDuration implements ExportInterface { - protected EntityManagerInterface $entityManager; + private ActivityRepository $activityRepository; public const SUM = 'sum'; @@ -30,13 +31,15 @@ class StatActivityDuration implements ExportInterface /** * @param string $action the stat to perform */ - public function __construct(EntityManagerInterface $em, string $action = 'sum') - { - $this->entityManager = $em; + public function __construct( + ActivityRepository $activityRepository, + string $action = 'sum' + ) { $this->action = $action; + $this->activityRepository = $activityRepository; } - public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder) { } @@ -68,7 +71,7 @@ class StatActivityDuration implements ExportInterface $acl ); - $qb = $this->entityManager->createQueryBuilder(); + $qb = $this->activityRepository->createQueryBuilder('activity'); $select = null; @@ -77,7 +80,6 @@ class StatActivityDuration implements ExportInterface } return $qb->select($select) - ->from('ChillActivityBundle:Activity', 'activity') ->join('activity.person', 'person') ->join('person.center', 'center') ->where($qb->expr()->in('center', ':centers')) @@ -96,7 +98,7 @@ class StatActivityDuration implements ExportInterface public function getAllowedFormattersTypes() { - return array(\Chill\MainBundle\Export\FormatterInterface::TYPE_TABULAR); + return [FormatterInterface::TYPE_TABULAR]; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php index dca4cec30..2e0149a38 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php @@ -1,25 +1,12 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; use Chill\MainBundle\Export\FilterInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -28,34 +15,24 @@ use Chill\MainBundle\Form\Type\Export\FilterType; use Doctrine\ORM\Query\Expr; use Symfony\Component\Translation\TranslatorInterface; -/** - * - * - * @author Julien Fastré - */ class ActivityDateFilter implements FilterInterface { - /** - * - * @var TranslatorInterface - */ - protected $translator; - + protected TranslatorInterface $translator; + function __construct(TranslatorInterface $translator) { $this->translator = $translator; } - public function addRole() { return null; } - public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) + public function alterQuery(QueryBuilder $qb, $data) { $where = $qb->getDQLPart('where'); - $clause = $qb->expr()->between('activity.date', ':date_from', + $clause = $qb->expr()->between('activity.date', ':date_from', ':date_to'); if ($where instanceof Expr\Andx) { @@ -63,7 +40,7 @@ class ActivityDateFilter implements FilterInterface } else { $where = $qb->expr()->andX($clause); } - + $qb->add('where', $where); $qb->setParameter('date_from', $data['date_from']); $qb->setParameter('date_to', $data['date_to']); @@ -74,35 +51,43 @@ class ActivityDateFilter implements FilterInterface return 'activity'; } - public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder) { - $builder->add('date_from', DateType::class, array( - 'label' => "Activities after this date", - 'data' => new \DateTime(), - 'attr' => array('class' => 'datepicker'), - 'widget'=> 'single_text', - 'format' => 'dd-MM-yyyy', - )); - - $builder->add('date_to', DateType::class, array( - 'label' => "Activities before this date", - 'data' => new \DateTime(), - 'attr' => array('class' => 'datepicker'), - 'widget'=> 'single_text', - 'format' => 'dd-MM-yyyy', - )); - + $builder->add( + 'date_from', + DateType::class, + [ + 'label' => 'Activities after this date', + 'data' => new \DateTime(), + 'attr' => ['class' => 'datepicker'], + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy', + ] + ); + + $builder->add( + 'date_to', + DateType::class, + [ + 'label' => 'Activities before this date', + 'data' => new \DateTime(), + 'attr' => ['class' => 'datepicker'], + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy', + ] + ); + $builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) { /* @var $filterForm \Symfony\Component\Form\FormInterface */ $filterForm = $event->getForm()->getParent(); $enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData(); - + if ($enabled === true) { // if the filter is enabled, add some validation $form = $event->getForm(); $date_from = $form->get('date_from')->getData(); $date_to = $form->get('date_to')->getData(); - + // check that fields are not empty if ($date_from === null) { $form->get('date_from')->addError(new FormError( @@ -113,8 +98,8 @@ class ActivityDateFilter implements FilterInterface $form->get('date_to')->addError(new FormError( $this->translator->trans('This field ' . 'should not be empty'))); - } - + } + // check that date_from is before date_to if ( ($date_from !== null && $date_to !== null) @@ -132,17 +117,18 @@ class ActivityDateFilter implements FilterInterface public function describeAction($data, $format = 'string') { - return array( - "Filtered by date of activity: only between %date_from% and %date_to%", - array( - "%date_from%" => $data['date_from']->format('d-m-Y'), - '%date_to%' => $data['date_to']->format('d-m-Y') - )); + return [ + 'Filtered by date of activity: only between %date_from% and %date_to%', + [ + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + ] + ]; } public function getTitle() { - return "Filtered by date activity"; + return 'Filtered by date activity'; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityReasonFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityReasonFilter.php index 37157af60..dbdceaaab 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityReasonFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityReasonFilter.php @@ -1,25 +1,12 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; +use Chill\ActivityBundle\Repository\ActivityReasonRepository; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -28,41 +15,24 @@ use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface; -/** - * - * - * @author Julien Fastré - */ -class ActivityReasonFilter implements FilterInterface, - ExportElementValidatedInterface +class ActivityReasonFilter implements FilterInterface, ExportElementValidatedInterface { - /** - * - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - /** - * The repository for activity reasons - * - * @var EntityRepository - */ - protected $reasonRepository; - + protected TranslatableStringHelperInterface $translatableStringHelper; + + protected ActivityReasonRepository $activityReasonRepository; + public function __construct( - TranslatableStringHelper $helper, - EntityRepository $reasonRepository + TranslatableStringHelper $helper, + ActivityReasonRepository $activityReasonRepository ) { $this->translatableStringHelper = $helper; - $this->reasonRepository = $reasonRepository; + $this->activityReasonRepository = $activityReasonRepository; } - - + public function alterQuery(QueryBuilder $qb, $data) { $where = $qb->getDQLPart('where'); @@ -75,12 +45,12 @@ class ActivityReasonFilter implements FilterInterface, && !$this->checkJoinAlreadyDefined($join['activity'], 'reasons') ) - OR + || (! array_key_exists('activity', $join)) ) { $qb->add( - 'join', - array('activity' => new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons')), + 'join', + array('activity' => new Join(Join::INNER_JOIN, 'activity.reasons', 'reasons')), true ); } @@ -90,25 +60,25 @@ class ActivityReasonFilter implements FilterInterface, } else { $where = $qb->expr()->andX($clause); } - + $qb->add('where', $where); $qb->setParameter('selected_activity_reasons', $data['reasons']); } - + /** * Check if a join between Activity and Reason is already defined - * + * * @param Join[] $joins * @return boolean */ - private function checkJoinAlreadyDefined(array $joins, $alias) + private function checkJoinAlreadyDefined(array $joins, $alias): bool { foreach ($joins as $join) { if ($join->getAlias() === $alias) { return true; } } - + return false; } @@ -119,51 +89,47 @@ class ActivityReasonFilter implements FilterInterface, public function buildForm(FormBuilderInterface $builder) { - //create a local copy of translatableStringHelper - $helper = $this->translatableStringHelper; - - $builder->add('reasons', EntityType::class, array( - 'class' => 'ChillActivityBundle:ActivityReason', - 'choice_label' => function (ActivityReason $reason) use ($helper) { - return $helper->localize($reason->getName()); - }, - 'group_by' => function(ActivityReason $reason) use ($helper) { - return $helper->localize($reason->getCategory()->getName()); - }, + $builder->add('reasons', EntityType::class, [ + 'class' => ActivityReason::class, + 'choice_label' => fn(ActivityReason $reason) => $this->translatableStringHelper->localize($reason->getName()), + 'group_by' => fn(ActivityReason $reason) => $this->translatableStringHelper->localize($reason->getCategory()->getName()), 'multiple' => true, 'expanded' => false - )); + ]); } - + public function validateForm($data, ExecutionContextInterface $context) { if ($data['reasons'] === null || count($data['reasons']) === 0) { - $context->buildViolation("At least one reason must be choosen") + $context + ->buildViolation('At least one reason must be chosen') ->addViolation(); } } - public function getTitle() + public function getTitle() { return 'Filter by reason'; } - + public function addRole() { return new Role(ActivityStatsVoter::STATS); } - + public function describeAction($data, $format = 'string') { // collect all the reasons'name used in this filter in one array $reasonsNames = array_map( - function(ActivityReason $r) { - return "\"".$this->translatableStringHelper->localize($r->getName())."\""; - }, - $this->reasonRepository->findBy(array('id' => $data['reasons']->toArray())) - ); - - return array("Filtered by reasons: only %list%", - ["%list%" => implode(", ", $reasonsNames)]); + fn(ActivityReason $r): string => '"' . $this->translatableStringHelper->localize($r->getName()) . '"', + $this->activityReasonRepository->findBy(array('id' => $data['reasons']->toArray())) + ); + + return [ + 'Filtered by reasons: only %list%', + [ + '%list%' => implode(", ", $reasonsNames), + ] + ]; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php index b3616da10..7bf1e7210 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php @@ -1,67 +1,37 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; +use Chill\ActivityBundle\Repository\ActivityTypeRepository; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; -use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr; use Symfony\Component\Security\Core\Role\Role; use Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter; -use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\ActivityBundle\Entity\ActivityType; -/** - * - * - */ -class ActivityTypeFilter implements FilterInterface, - ExportElementValidatedInterface +class ActivityTypeFilter implements FilterInterface, ExportElementValidatedInterface { - /** - * - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - /** - * The repository for activity reasons - * - * @var EntityRepository - */ - protected $typeRepository; - + protected TranslatableStringHelperInterface $translatableStringHelper; + + protected ActivityTypeRepository $activityTypeRepository; + public function __construct( - TranslatableStringHelper $helper, - EntityRepository $typeRepository + TranslatableStringHelperInterface $translatableStringHelper, + ActivityTypeRepository $activityTypeRepository ) { - $this->translatableStringHelper = $helper; - $this->typeRepository = $typeRepository; + $this->translatableStringHelper = $translatableStringHelper; + $this->activityTypeRepository = $activityTypeRepository; } - - + public function alterQuery(QueryBuilder $qb, $data) { $where = $qb->getDQLPart('where'); @@ -72,14 +42,14 @@ class ActivityTypeFilter implements FilterInterface, } else { $where = $qb->expr()->andX($clause); } - + $qb->add('where', $where); $qb->setParameter('selected_activity_types', $data['types']); } - + /** * Check if a join between Activity and Reason is already defined - * + * * @param Join[] $joins * @return boolean */ @@ -90,7 +60,7 @@ class ActivityTypeFilter implements FilterInterface, return true; } } - + return false; } @@ -101,48 +71,50 @@ class ActivityTypeFilter implements FilterInterface, public function buildForm(FormBuilderInterface $builder) { - //create a local copy of translatableStringHelper - $helper = $this->translatableStringHelper; - - $builder->add('types', EntityType::class, array( - 'class' => ActivityType::class, - 'choice_label' => function (ActivityType $type) use ($helper) { - return $helper->localize($type->getName()); - }, - 'multiple' => true, - 'expanded' => false - )); + $builder->add( + 'types', + EntityType::class, + [ + 'class' => ActivityType::class, + 'choice_label' => fn(ActivityType $type) => $this->translatableStringHelper->localize($type->getName()), + 'multiple' => true, + 'expanded' => false + ] + ); } - + public function validateForm($data, ExecutionContextInterface $context) { if ($data['types'] === null || count($data['types']) === 0) { - $context->buildViolation("At least one type must be choosen") + $context + ->buildViolation('At least one type must be chosen') ->addViolation(); } } - public function getTitle() + public function getTitle() { return 'Filter by activity type'; } - + public function addRole() { return new Role(ActivityStatsVoter::STATS); } - + public function describeAction($data, $format = 'string') { // collect all the reasons'name used in this filter in one array $reasonsNames = array_map( - function(ActivityType $t) { - return "\"".$this->translatableStringHelper->localize($t->getName())."\""; - }, - $this->typeRepository->findBy(array('id' => $data['types']->toArray())) - ); - - return array("Filtered by activity type: only %list%", - ["%list%" => implode(", ", $reasonsNames)]); + fn(ActivityType $t): string => '"' . $this->translatableStringHelper->localize($t->getName()) . '"', + $this->activityTypeRepository->findBy(['id' => $data['types']->toArray()]) + ); + + return [ + 'Filtered by activity type: only %list%', + [ + '%list%' => implode(", ", $reasonsNames), + ] + ]; } } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/PersonHavingActivityBetweenDateFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/PersonHavingActivityBetweenDateFilter.php index cf444bd0a..d150c68e6 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/PersonHavingActivityBetweenDateFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/PersonHavingActivityBetweenDateFilter.php @@ -1,25 +1,14 @@ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Export\Filter; +use Chill\ActivityBundle\Repository\ActivityReasonRepository; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -29,78 +18,58 @@ use Doctrine\ORM\Query\Expr; use Chill\MainBundle\Templating\TranslatableStringHelper; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Chill\ActivityBundle\Entity\ActivityReason; -use Doctrine\ORM\EntityRepository; -use Doctrine\ORM\EntityManager; use Chill\PersonBundle\Export\Declarations; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Chill\MainBundle\Export\ExportElementValidatedInterface; -/** - * - * - * @author Julien Fastré - */ -class PersonHavingActivityBetweenDateFilter implements FilterInterface, - ExportElementValidatedInterface +class PersonHavingActivityBetweenDateFilter implements FilterInterface, ExportElementValidatedInterface { - - /** - * - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - /** - * - * @var EntityRepository - */ - protected $activityReasonRepository; - - /** - * - * @var TranslatorInterface - */ - protected $translator; - + protected TranslatableStringHelperInterface $translatableStringHelper; + + protected ActivityReasonRepository $activityReasonRepository; + + protected TranslatorInterface $translator; + public function __construct( - TranslatableStringHelper $translatableStringHelper, - EntityRepository $activityReasonRepository, - TranslatorInterface $translator + TranslatableStringHelper $translatableStringHelper, + ActivityReasonRepository $activityReasonRepository, + TranslatorInterface $translator ) { $this->translatableStringHelper = $translatableStringHelper; $this->activityReasonRepository = $activityReasonRepository; - $this->translator = $translator; + $this->translator = $translator; } - public function addRole() { return null; } - public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) + public function alterQuery(QueryBuilder $qb, $data) { // create a query for activity $sqb = $qb->getEntityManager()->createQueryBuilder(); - $sqb->select("person_person_having_activity.id") - ->from("ChillActivityBundle:Activity", "activity_person_having_activity") - ->join("activity_person_having_activity.person", "person_person_having_activity") - ; + $sqb->select('person_person_having_activity.id') + ->from('ChillActivityBundle:Activity', 'activity_person_having_activity') + ->join('activity_person_having_activity.person', 'person_person_having_activity'); + // add clause between date - $sqb->where("activity_person_having_activity.date BETWEEN " - . ":person_having_activity_between_date_from" - . " AND " - . ":person_having_activity_between_date_to"); + $sqb->where('activity_person_having_activity.date BETWEEN ' + . ':person_having_activity_between_date_from' + . ' AND ' + . ':person_having_activity_between_date_to'); + // add clause activity reason - $sqb->join('activity_person_having_activity.reasons', - 'reasons_person_having_activity'); + $sqb->join('activity_person_having_activity.reasons', 'reasons_person_having_activity'); + $sqb->andWhere( - $sqb->expr()->in( - 'reasons_person_having_activity', - ":person_having_activity_reasons") - ); - + $sqb->expr()->in( + 'reasons_person_having_activity', ':person_having_activity_reasons' + ) + ); + $where = $qb->getDQLPart('where'); $clause = $qb->expr()->in('person.id', $sqb->getDQL()); @@ -109,11 +78,11 @@ class PersonHavingActivityBetweenDateFilter implements FilterInterface, } else { $where = $qb->expr()->andX($clause); } - + $qb->add('where', $where); - $qb->setParameter('person_having_activity_between_date_from', + $qb->setParameter('person_having_activity_between_date_from', $data['date_from']); - $qb->setParameter('person_having_activity_between_date_to', + $qb->setParameter('person_having_activity_between_date_to', $data['date_to']); $qb->setParameter('person_having_activity_reasons', $data['reasons']); } @@ -123,51 +92,45 @@ class PersonHavingActivityBetweenDateFilter implements FilterInterface, return Declarations::PERSON_IMPLIED_IN; } - public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder) { - $builder->add('date_from', DateType::class, array( - 'label' => "Implied in an activity after this date", + $builder->add('date_from', DateType::class, [ + 'label' => 'Implied in an activity after this date', 'data' => new \DateTime(), - 'attr' => array('class' => 'datepicker'), + 'attr' => ['class' => 'datepicker'], 'widget'=> 'single_text', 'format' => 'dd-MM-yyyy', - )); - - $builder->add('date_to', DateType::class, array( - 'label' => "Implied in an activity before this date", + ]); + + $builder->add('date_to', DateType::class, [ + 'label' => 'Implied in an activity before this date', 'data' => new \DateTime(), - 'attr' => array('class' => 'datepicker'), + 'attr' => ['class' => 'datepicker'], 'widget'=> 'single_text', 'format' => 'dd-MM-yyyy', - )); - - $builder->add('reasons', EntityType::class, array( - 'class' => 'ChillActivityBundle:ActivityReason', - 'choice_label' => function (ActivityReason $reason) { - return $this->translatableStringHelper - ->localize($reason->getName()); - }, - 'group_by' => function(ActivityReason $reason) { - return $this->translatableStringHelper - ->localize($reason->getCategory()->getName()); - }, + ]); + + $builder->add('reasons', EntityType::class, [ + 'class' => ActivityReason::class, + 'choice_label' => fn (ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getName()), + 'group_by' => fn(ActivityReason $reason): ?string => $this->translatableStringHelper->localize($reason->getCategory()->getName()), 'data' => $this->activityReasonRepository->findAll(), 'multiple' => true, 'expanded' => false, - 'label' => "Activity reasons for those activities" - )); - + 'label' => 'Activity reasons for those activities' + ]); + $builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) { - /* @var $filterForm \Symfony\Component\Form\FormInterface */ + /* @var FormInterface $filterForm */ $filterForm = $event->getForm()->getParent(); $enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData(); - + if ($enabled === true) { // if the filter is enabled, add some validation $form = $event->getForm(); $date_from = $form->get('date_from')->getData(); $date_to = $form->get('date_to')->getData(); - + // check that fields are not empty if ($date_from === null) { $form->get('date_from')->addError(new FormError( @@ -178,8 +141,8 @@ class PersonHavingActivityBetweenDateFilter implements FilterInterface, $form->get('date_to')->addError(new FormError( $this->translator->trans('This field ' . 'should not be empty'))); - } - + } + // check that date_from is before date_to if ( ($date_from !== null && $date_to !== null) @@ -194,35 +157,37 @@ class PersonHavingActivityBetweenDateFilter implements FilterInterface, } }); } - + public function validateForm($data, ExecutionContextInterface $context) { if ($data['reasons'] === null || count($data['reasons']) === 0) { - $context->buildViolation("At least one reason must be choosen") + $context->buildViolation('At least one reason must be chosen') ->addViolation(); } } public function describeAction($data, $format = 'string') { - return array( - "Filtered by person having an activity between %date_from% and " - . "%date_to% with reasons %reasons_name%", - array( - "%date_from%" => $data['date_from']->format('d-m-Y'), + return [ + 'Filtered by person having an activity between %date_from% and ' + . '%date_to% with reasons %reasons_name%', + [ + '%date_from%' => $data['date_from']->format('d-m-Y'), '%date_to%' => $data['date_to']->format('d-m-Y'), - "%reasons_name%" => implode(", ", array_map( - function (ActivityReason $r) { - return '"'.$this->translatableStringHelper-> - localize($r->getName()).'"'; - }, - $data['reasons'])) - )); + '%reasons_name%' => implode( + ", ", + array_map( + fn(ActivityReason $r): string => '"' . $this->translatableStringHelper->localize($r->getName()) . '"', + $data['reasons'] + ) + ) + ] + ]; } public function getTitle() { - return "Filtered by person having an activity in a period"; + return 'Filtered by person having an activity in a period'; } } diff --git a/src/Bundle/ChillActivityBundle/Form/Type/TranslatableActivityType.php b/src/Bundle/ChillActivityBundle/Form/Type/TranslatableActivityType.php index fa4b23212..f930d3c1a 100644 --- a/src/Bundle/ChillActivityBundle/Form/Type/TranslatableActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/Type/TranslatableActivityType.php @@ -1,56 +1,29 @@ , - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Form\Type; +use Chill\ActivityBundle\Repository\ActivityTypeRepository; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Bridge\Doctrine\Form\Type\EntityType; -use Chill\MainBundle\Templating\TranslatableStringHelper; -use Doctrine\ORM\EntityRepository; use Chill\ActivityBundle\Entity\ActivityType; -/** - * Description of TranslatableActivityType - * - * @author Champs-Libres Coop - */ class TranslatableActivityType extends AbstractType { + protected TranslatableStringHelperInterface $translatableStringHelper; - /** - * - * @var TranslatableStringHelper - */ - protected $translatableStringHelper; - - protected $activityTypeRepository; + protected ActivityTypeRepository $activityTypeRepository; public function __construct( - TranslatableStringHelper $helper, - EntityRepository $activityTypeRepository - ) - { + TranslatableStringHelperInterface $helper, + ActivityTypeRepository $activityTypeRepository + ) { $this->translatableStringHelper = $helper; $this->activityTypeRepository = $activityTypeRepository; } @@ -65,22 +38,21 @@ class TranslatableActivityType extends AbstractType return EntityType::class; } - public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options) { - /* @var $qb \Doctrine\ORM\QueryBuilder */ + public function buildForm(FormBuilderInterface $builder, array $options) { + /* @var QueryBuilder $qb */ $qb = $options['query_builder']; if ($options['active_only'] === true) { $qb->where($qb->expr()->eq('at.active', ':active')); - $qb->setParameter('active', true, \Doctrine\DBAL\Types\Types::BOOLEAN); + $qb->setParameter('active', true, Types::BOOLEAN); } } public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefaults( array( - 'class' => 'ChillActivityBundle:ActivityType', + 'class' => ActivityType::class, 'active_only' => true, 'query_builder' => $this->activityTypeRepository ->createQueryBuilder('at'), diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityReasonCategoryRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityReasonCategoryRepository.php new file mode 100644 index 000000000..3026105b0 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityReasonCategoryRepository.php @@ -0,0 +1,23 @@ +, - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ +declare(strict_types=1); namespace Chill\ActivityBundle\Repository; @@ -29,10 +11,10 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; /** - * @method AccompanyingPeriodParticipation|null find($id, $lockMode = null, $lockVersion = null) - * @method AccompanyingPeriodParticipation|null findOneBy(array $criteria, array $orderBy = null) - * @method AccompanyingPeriodParticipation[] findAll() - * @method AccompanyingPeriodParticipation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * @method Activity|null find($id, $lockMode = null, $lockVersion = null) + * @method Activity|null findOneBy(array $criteria, array $orderBy = null) + * @method Activity[] findAll() + * @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class ActivityRepository extends ServiceEntityRepository { @@ -42,12 +24,7 @@ class ActivityRepository extends ServiceEntityRepository } /** - * @param $person - * @param array $scopes - * @param string[] $orderBy - * @param int $limit - * @param int $offset - * @return array|Activity[] + * @return Activity[] */ public function findByPersonImplied(Person $person, array $scopes, ?array $orderBy = [ 'date' => 'DESC'], ?int $limit = 100, ?int $offset = 0): array { @@ -63,8 +40,7 @@ class ActivityRepository extends ServiceEntityRepository ':person MEMBER OF a.persons' ) ) - ->setParameter('person', $person) - ; + ->setParameter('person', $person); foreach ($orderBy as $k => $dir) { $qb->addOrderBy('a.'.$k, $dir); @@ -72,17 +48,11 @@ class ActivityRepository extends ServiceEntityRepository $qb->setMaxResults($limit)->setFirstResult($offset); - return $qb->getQuery() - ->getResult(); + return $qb->getQuery()->getResult(); } /** - * @param AccompanyingPeriod $period - * @param array $scopes - * @param int|null $limit - * @param int|null $offset - * @param array|string[] $orderBy - * @return array|Activity[] + * @return Activity[] */ public function findByAccompanyingPeriod(AccompanyingPeriod $period, array $scopes, ?bool $allowNullScope = false, ?int $limit = 100, ?int $offset = 0, array $orderBy = ['date' => 'desc']): array { @@ -92,8 +62,7 @@ class ActivityRepository extends ServiceEntityRepository if (!$allowNullScope) { $qb ->where($qb->expr()->in('a.scope', ':scopes')) - ->setParameter('scopes', $scopes) - ; + ->setParameter('scopes', $scopes); } else { $qb ->where( @@ -102,16 +71,14 @@ class ActivityRepository extends ServiceEntityRepository $qb->expr()->isNull('a.scope') ) ) - ->setParameter('scopes', $scopes) - ; + ->setParameter('scopes', $scopes); } $qb ->andWhere( $qb->expr()->eq('a.accompanyingPeriod', ':period') ) - ->setParameter('period', $period) - ; + ->setParameter('period', $period); foreach ($orderBy as $k => $dir) { $qb->addOrderBy('a.'.$k, $dir); @@ -119,7 +86,6 @@ class ActivityRepository extends ServiceEntityRepository $qb->setMaxResults($limit)->setFirstResult($offset); - return $qb->getQuery() - ->getResult(); + return $qb->getQuery()->getResult(); } } diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityTypeCategoryRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeCategoryRepository.php new file mode 100644 index 000000000..62a6a9a0d --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityTypeCategoryRepository.php @@ -0,0 +1,23 @@ +
-
    +
    • - {{ p.text }} + + + {{ p.text }} +
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups/PersonBadge.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups/PersonBadge.vue index bef11159c..0b2dd6613 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups/PersonBadge.vue +++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/ConcernedGroups/PersonBadge.vue @@ -4,7 +4,7 @@ {{ textCutted }} - diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig index 1e1d65d91..f4a3d4d29 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig @@ -27,14 +27,16 @@ {{ activity.type.name | localize_translatable_string }}