* * 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\MainBundle\Timeline; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; /** * Build timeline * * @author Julien Fastré */ class TimelineBuilder implements ContainerAwareInterface { use \Symfony\Component\DependencyInjection\ContainerAwareTrait; /** * * @var \Doctrine\ORM\EntityManagerInterface */ private $em; /** * Record provider * * This array has the structure `[ 'service id' => $service ]` * * @var TimelineProviderInterface[] */ private $providers = []; /** * Record provider and their context * * This array has the structure `[ 'context' => [ 'service id' ] ]` * * @var array */ private $providersByContext = []; public function __construct(EntityManagerInterface $em) { $this->em = $em; } /** * return an HTML string with timeline * * This function must be called from controller * * @example https://redmine.champs-libres.coop/projects/chillperson/repository/revisions/bd2e1b1808f73e39532e9538413025df5487cad0/entry/Controller/TimelinePersonController.php#L47 the implementation in person bundle * * @param string $context * @param array $args arguments defined by the bundle which create the context * @param int $firstItem first item number * @param int $number number of items by page * @return string an HTML representation, must be included using `|raw` filter */ public function getTimelineHTML($context, array $args, $firstItem = 0, $number = 20) { $union = $this->buildUnionQuery($context, $args); //add ORDER BY clause and LIMIT $query = $union . sprintf(' ORDER BY date DESC LIMIT %d OFFSET %d', $number, $firstItem); // run query and handle results $fetched = $this->runUnionQuery($query); $entitiesByKey = $this->getEntities($fetched, $context); return $this->render($fetched, $entitiesByKey, $context, $args); } /** * Return the number of items for the given context and args * * @param unknown $context * @param array $args * @return mixed|\Doctrine\DBAL\Driver\Statement|NULL */ public function countItems($context, array $args) { $union = $this->buildUnionQuery($context, $args); // embed the union query inside a count query $count = sprintf('SELECT COUNT(sq.id) AS total FROM (%s) as sq', $union); $rsm = (new ResultSetMapping()) ->addScalarResult('total', 'total', Type::INTEGER); return $this->em->createNativeQuery($count, $rsm) ->getSingleScalarResult(); } /** * add a provider id * * @internal This function is called by the TimelineCompilerClass * * @param string $context the context of the service * @param string $id the */ public function addProvider($context, $id, TimelineProviderInterface $provider) { $this->providersByContext[$context][] = $id; $this->providers[$id] = $provider; } /** * Get providers by context * * @param string $context * @return TimelineProviderInterface[] */ public function getProvidersByContext($context) { //throw an exception if no provider have been defined for this context if (!array_key_exists($context, $this->providersByContext)) { throw new \LogicException(sprintf('No builders have been defined for "%s"' . ' context', $context)); } $providers = []; foreach($this->providersByContext[$context] as $providerId) { $providers[] = $this->providers[$providerId]; } return $providers; } /** * build the UNION query with all providers * * @uses self::buildSelectQuery to build individual SELECT queries * * @param string $context * @param mixed $args * @param int $page * @param int $number * @return string * @throws \LogicException if no builder have been defined for this context */ private function buildUnionQuery($context, array $args) { //append SELECT queries with UNION keyword between them $union = ''; foreach($this->getProvidersByContext($context) as $provider) { $select = $this->buildSelectQuery($provider, $context, $args); $append = ($union === '') ? $select : ' UNION '.$select; $union .= $append; } return $union; } /** * return the SQL SELECT query as a string, * * @uses TimelineProfiderInterface::fetchQuery use the fetchQuery function * @param \Chill\MainBundle\Timeline\TimelineProviderInterface $provider * @param string $context * @param mixed[] $args * @return string */ private function buildSelectQuery(TimelineProviderInterface $provider, $context, array $args) { $data = $provider->fetchQuery($context, $args); return sprintf( 'SELECT %s AS id, ' . '%s AS "date", ' . "'%s' AS type " . 'FROM %s ' . 'WHERE %s', $data['id'], $data['date'], $data['type'], $data['FROM'], $data['WHERE']); } /** * run the UNION query and return result as an array * * @param string $query * @return array */ private function runUnionQuery($query) { $resultSetMapping = (new ResultSetMapping()) ->addScalarResult('id', 'id') ->addScalarResult('type', 'type') ->addScalarResult('date', 'date'); return $this->em->createNativeQuery($query, $resultSetMapping) ->getArrayResult(); } /** * * @param array $queriedIds * @param string $context * @return array with the form array($type => [$entity, $entity, $entity]) */ private function getEntities(array $queriedIds, $context) { //gather entities by type to pass all id with same type to the TimelineProvider. $idsByType = array(); foreach($queriedIds as $result) { $idsByType[$result['type']][] = $result['id']; } //fetch entities from providers $entitiesByType = array(); foreach ($idsByType as $type => $ids) { //iterate providers for current context foreach($this->getProvidersByContext($context) as $provider) { if ($provider->supportsType($type)) { $entitiesByType[$type] = $provider->getEntities($ids); break; //we assume that providers have unique keys => we break the loop } } } return $entitiesByType; } /** * render the timeline as HTML * * @param array $fetched * @param array $entitiesByType * @param string $context * @param mixed[] $args * @return string the HTML representation of the timeline */ private function render(array $fetched, array $entitiesByType, $context, array $args) { //add results to a pretty array $timelineEntries = array(); foreach ($fetched as $result) { $data = $this->getTemplateData( $result['type'], $entitiesByType[$result['type']][$result['id']], //the entity $context, $args); $timelineEntry['date'] = new \DateTime($result['date']); $timelineEntry['template'] = $data['template']; $timelineEntry['template_data'] = $data['template_data']; $timelineEntries[] = $timelineEntry; } return $this->container->get('templating') ->render('@ChillMain/Timeline/index.html.twig', array( 'results' => $timelineEntries )); } /** * get the template data from the provider for the given entity, by type. * * @param string $type * @param mixed $entity * @param string $context * @param mixed[] $args * @return array the template data fetched from the provider */ private function getTemplateData($type, $entity, $context, array $args) { foreach($this->getProvidersByContext($context) as $provider) { if ($provider->supportsType($type)) { return $provider->getEntityTemplate($entity, $context, $args); } } } }