2019-11-16 19:23:45 +01:00
< ? php
2020-01-05 15:46:58 +01:00
2020-01-17 21:38:36 +01:00
/*
* Symfony DataTables Bundle
* ( c ) Omines Internetbureau B . V . - https :// omines . nl /
2019-11-16 19:23:45 +01:00
*
2020-01-17 21:38:36 +01:00
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
2019-11-16 19:23:45 +01:00
*/
2020-01-17 21:38:36 +01:00
declare ( strict_types = 1 );
2019-11-16 19:23:45 +01:00
namespace App\DataTables\Adapter ;
2020-01-16 21:42:29 +01:00
use Doctrine\Common\Persistence\ManagerRegistry ;
use Doctrine\ORM\EntityManager ;
2019-11-16 19:23:45 +01:00
use Doctrine\ORM\Query ;
use Doctrine\ORM\QueryBuilder ;
2020-01-16 21:42:29 +01:00
use Omines\DataTablesBundle\Adapter\AbstractAdapter ;
2019-11-16 19:23:45 +01:00
use Omines\DataTablesBundle\Adapter\AdapterQuery ;
2020-01-16 21:42:29 +01:00
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent ;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\AutomaticQueryBuilder ;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\QueryBuilderProcessorInterface ;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider ;
2019-11-16 19:23:45 +01:00
use Omines\DataTablesBundle\Column\AbstractColumn ;
2020-01-16 21:42:29 +01:00
use Omines\DataTablesBundle\DataTableState ;
use Omines\DataTablesBundle\Exception\InvalidConfigurationException ;
use Omines\DataTablesBundle\Exception\MissingDependencyException ;
use Symfony\Component\OptionsResolver\Options ;
use Symfony\Component\OptionsResolver\OptionsResolver ;
2019-11-16 19:23:45 +01:00
/**
2020-01-17 21:38:36 +01:00
* ORMAdapter .
*
* @ author Niels Keurentjes < niels . keurentjes @ omines . com >
* @ author Robbert Beesems < robbert . beesems @ omines . com >
2019-11-16 19:23:45 +01:00
*/
2020-01-17 21:38:36 +01:00
class ORMAdapter extends AbstractAdapter
2019-11-16 19:23:45 +01:00
{
2020-01-16 21:42:29 +01:00
/** @var EntityManager */
2020-01-17 21:38:36 +01:00
protected $manager ;
2020-01-16 21:42:29 +01:00
/** @var \Doctrine\ORM\Mapping\ClassMetadata */
2020-01-17 21:38:36 +01:00
protected $metadata ;
2020-01-16 21:42:29 +01:00
2020-02-01 16:17:20 +01:00
/** @var QueryBuilderProcessorInterface[] */
protected $criteriaProcessors ;
/** @var ManagerRegistry */
private $registry ;
2020-01-16 21:42:29 +01:00
/** @var int */
private $hydrationMode ;
/** @var QueryBuilderProcessorInterface[] */
private $queryBuilderProcessors ;
/**
* DoctrineAdapter constructor .
*/
2020-02-01 16:17:20 +01:00
public function __construct ( ? ManagerRegistry $registry = null )
2019-11-16 19:23:45 +01:00
{
2020-01-16 21:42:29 +01:00
if ( null === $registry ) {
throw new MissingDependencyException ( 'Install doctrine/doctrine-bundle to use the ORMAdapter' );
}
parent :: __construct ();
$this -> registry = $registry ;
2019-11-16 19:23:45 +01:00
}
2020-01-04 20:24:09 +01:00
2020-02-01 16:17:20 +01:00
public function configure ( array $options ) : void
2019-11-16 19:23:45 +01:00
{
2020-01-16 21:42:29 +01:00
$resolver = new OptionsResolver ();
$this -> configureOptions ( $resolver );
$options = $resolver -> resolve ( $options );
// Enable automated mode or just get the general default entity manager
if ( null === ( $this -> manager = $this -> registry -> getManagerForClass ( $options [ 'entity' ]))) {
throw new InvalidConfigurationException ( sprintf ( 'Doctrine has no manager for entity "%s", is it correctly imported and referenced?' , $options [ 'entity' ]));
}
$this -> metadata = $this -> manager -> getClassMetadata ( $options [ 'entity' ]);
if ( empty ( $options [ 'query' ])) {
$options [ 'query' ] = [ new AutomaticQueryBuilder ( $this -> manager , $this -> metadata )];
}
// Set options
$this -> hydrationMode = $options [ 'hydrate' ];
$this -> queryBuilderProcessors = $options [ 'query' ];
$this -> criteriaProcessors = $options [ 'criteria' ];
2019-11-16 19:23:45 +01:00
}
2020-01-04 20:24:09 +01:00
2020-02-01 16:17:20 +01:00
public function addCriteriaProcessor ( $processor ) : void
2020-01-16 21:42:29 +01:00
{
$this -> criteriaProcessors [] = $this -> normalizeProcessor ( $processor );
}
2020-02-01 16:17:20 +01:00
protected function prepareQuery ( AdapterQuery $query ) : void
2020-01-16 21:42:29 +01:00
{
$state = $query -> getState ();
$query -> set ( 'qb' , $builder = $this -> createQueryBuilder ( $state ));
$query -> set ( 'rootAlias' , $rootAlias = $builder -> getDQLPart ( 'from' )[ 0 ] -> getAlias ());
// Provide default field mappings if needed
foreach ( $state -> getDataTable () -> getColumns () as $column ) {
if ( null === $column -> getField () && isset ( $this -> metadata -> fieldMappings [ $name = $column -> getName ()])) {
$column -> setOption ( 'field' , " { $rootAlias } . { $name } " );
}
}
/** @var Query\Expr\From $fromClause */
$fromClause = $builder -> getDQLPart ( 'from' )[ 0 ];
$identifier = " { $fromClause -> getAlias () } . { $this -> metadata -> getSingleIdentifierFieldName () } " ;
2020-01-17 21:38:36 +01:00
$query -> setTotalRows ( $this -> getCount ( $builder , $identifier ));
2020-01-16 21:42:29 +01:00
// Get record count after filtering
$this -> buildCriteria ( $builder , $state );
2020-01-17 21:38:36 +01:00
$query -> setFilteredRows ( $this -> getCount ( $builder , $identifier ));
2020-01-16 21:42:29 +01:00
// Perform mapping of all referred fields and implied fields
$aliases = $this -> getAliases ( $query );
$query -> set ( 'aliases' , $aliases );
$query -> setIdentifierPropertyPath ( $this -> mapFieldToPropertyPath ( $identifier , $aliases ));
}
/**
* @ return array
*/
protected function getAliases ( AdapterQuery $query )
{
/** @var QueryBuilder $builder */
$builder = $query -> get ( 'qb' );
$aliases = [];
/** @var Query\Expr\From $from */
foreach ( $builder -> getDQLPart ( 'from' ) as $from ) {
$aliases [ $from -> getAlias ()] = [ null , $this -> manager -> getMetadataFactory () -> getMetadataFor ( $from -> getFrom ())];
}
// Alias all joins
foreach ( $builder -> getDQLPart ( 'join' ) as $joins ) {
/** @var Query\Expr\Join $join */
foreach ( $joins as $join ) {
if ( false === mb_strstr ( $join -> getJoin (), '.' )) {
continue ;
}
2020-02-01 16:17:20 +01:00
[ $origin , $target ] = explode ( '.' , $join -> getJoin ());
2020-01-16 21:42:29 +01:00
$mapping = $aliases [ $origin ][ 1 ] -> getAssociationMapping ( $target );
$aliases [ $join -> getAlias ()] = [ $join -> getJoin (), $this -> manager -> getMetadataFactory () -> getMetadataFor ( $mapping [ 'targetEntity' ])];
}
}
return $aliases ;
}
protected function mapPropertyPath ( AdapterQuery $query , AbstractColumn $column )
{
return $this -> mapFieldToPropertyPath ( $column -> getField (), $query -> get ( 'aliases' ));
}
protected function getResults ( AdapterQuery $query ) : \Traversable
2019-11-16 19:23:45 +01:00
{
/** @var QueryBuilder $builder */
$builder = $query -> get ( 'qb' );
$state = $query -> getState ();
2020-01-16 21:42:29 +01:00
2019-11-16 19:23:45 +01:00
// Apply definitive view state for current 'page' of the table
2020-02-01 16:17:20 +01:00
foreach ( $state -> getOrderBy () as [ $column , $direction ]) {
2019-11-16 19:23:45 +01:00
/** @var AbstractColumn $column */
if ( $column -> isOrderable ()) {
$builder -> addOrderBy ( $column -> getOrderField (), $direction );
}
}
if ( $state -> getLength () > 0 ) {
$builder
-> setFirstResult ( $state -> getStart ())
2020-01-17 21:38:36 +01:00
-> setMaxResults ( $state -> getLength ())
;
2019-11-16 19:23:45 +01:00
}
2020-01-16 21:42:29 +01:00
$query = $builder -> getQuery ();
$event = new ORMAdapterQueryEvent ( $query );
$state -> getDataTable () -> getEventDispatcher () -> dispatch ( $event , ORMAdapterEvents :: PRE_QUERY );
2020-01-17 21:38:36 +01:00
foreach ( $query -> iterate ([], $this -> hydrationMode ) as $result ) {
yield $entity = array_values ( $result )[ 0 ];
2020-01-16 21:42:29 +01:00
if ( Query :: HYDRATE_OBJECT === $this -> hydrationMode ) {
$this -> manager -> detach ( $entity );
}
}
}
2020-02-01 16:17:20 +01:00
protected function buildCriteria ( QueryBuilder $queryBuilder , DataTableState $state ) : void
2020-01-16 21:42:29 +01:00
{
foreach ( $this -> criteriaProcessors as $provider ) {
$provider -> process ( $queryBuilder , $state );
}
}
protected function createQueryBuilder ( DataTableState $state ) : QueryBuilder
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = $this -> manager -> createQueryBuilder ();
// Run all query builder processors in order
foreach ( $this -> queryBuilderProcessors as $processor ) {
$processor -> process ( $queryBuilder , $state );
}
return $queryBuilder ;
}
/**
* @ param $identifier
2020-02-01 16:17:20 +01:00
*
2020-01-16 21:42:29 +01:00
* @ return int
*/
2020-01-17 21:38:36 +01:00
protected function getCount ( QueryBuilder $queryBuilder , $identifier )
2020-01-16 21:42:29 +01:00
{
$qb = clone $queryBuilder ;
$qb -> resetDQLPart ( 'orderBy' );
$gb = $qb -> getDQLPart ( 'groupBy' );
2020-02-01 16:17:20 +01:00
if ( empty ( $gb ) || ! $this -> hasGroupByPart ( $identifier , $gb )) {
2020-01-16 21:42:29 +01:00
$qb -> select ( $qb -> expr () -> count ( $identifier ));
return ( int ) $qb -> getQuery () -> getSingleScalarResult ();
}
2020-02-01 16:17:20 +01:00
$qb -> resetDQLPart ( 'groupBy' );
$qb -> select ( $qb -> expr () -> countDistinct ( $identifier ));
return ( int ) $qb -> getQuery () -> getSingleScalarResult ();
2020-01-16 21:42:29 +01:00
}
/**
* @ param $identifier
* @ param Query\Expr\GroupBy [] $gbList
2020-02-01 16:17:20 +01:00
*
2020-01-16 21:42:29 +01:00
* @ return bool
*/
protected function hasGroupByPart ( $identifier , array $gbList )
{
foreach ( $gbList as $gb ) {
if ( in_array ( $identifier , $gb -> getParts (), true )) {
return true ;
}
}
return false ;
}
/**
* @ param string $field
2020-02-01 16:17:20 +01:00
*
2020-01-16 21:42:29 +01:00
* @ return string
*/
2020-01-17 21:38:36 +01:00
protected function mapFieldToPropertyPath ( $field , array $aliases = [])
2020-01-16 21:42:29 +01:00
{
$parts = explode ( '.' , $field );
if ( count ( $parts ) < 2 ) {
throw new InvalidConfigurationException ( sprintf ( " Field name '%s' must consist at least of an alias and a field separated with a period " , $field ));
}
2020-02-01 16:17:20 +01:00
[ $origin , $target ] = $parts ;
2020-01-16 21:42:29 +01:00
$path = [ $target ];
$current = $aliases [ $origin ][ 0 ];
while ( null !== $current ) {
2020-02-01 16:17:20 +01:00
[ $origin , $target ] = explode ( '.' , $current );
2020-01-16 21:42:29 +01:00
$path [] = $target ;
$current = $aliases [ $origin ][ 0 ];
}
if ( Query :: HYDRATE_ARRAY === $this -> hydrationMode ) {
2020-02-01 16:17:20 +01:00
return '[' . implode ( '][' , array_reverse ( $path )) . ']' ;
2020-01-16 21:42:29 +01:00
}
2020-02-01 16:17:20 +01:00
return implode ( '.' , array_reverse ( $path ));
2020-01-16 21:42:29 +01:00
}
2020-02-01 16:17:20 +01:00
protected function configureOptions ( OptionsResolver $resolver ) : void
2020-01-16 21:42:29 +01:00
{
$providerNormalizer = function ( Options $options , $value ) {
return array_map ([ $this , 'normalizeProcessor' ], ( array ) $value );
};
$resolver
-> setDefaults ([
2020-02-01 16:17:20 +01:00
'hydrate' => Query :: HYDRATE_OBJECT ,
'query' => [],
'criteria' => function ( Options $options ) {
return [ new SearchCriteriaProvider ()];
},
])
2020-01-16 21:42:29 +01:00
-> setRequired ( 'entity' )
-> setAllowedTypes ( 'entity' , [ 'string' ])
-> setAllowedTypes ( 'hydrate' , 'int' )
-> setAllowedTypes ( 'query' , [ QueryBuilderProcessorInterface :: class , 'array' , 'callable' ])
-> setAllowedTypes ( 'criteria' , [ QueryBuilderProcessorInterface :: class , 'array' , 'callable' , 'null' ])
-> setNormalizer ( 'query' , $providerNormalizer )
-> setNormalizer ( 'criteria' , $providerNormalizer )
;
}
/**
* @ param callable | QueryBuilderProcessorInterface $provider
2020-02-01 16:17:20 +01:00
*
2020-01-16 21:42:29 +01:00
* @ return QueryBuilderProcessorInterface
*/
private function normalizeProcessor ( $provider )
{
if ( $provider instanceof QueryBuilderProcessorInterface ) {
return $provider ;
} elseif ( is_callable ( $provider )) {
return new class ( $provider ) implements QueryBuilderProcessorInterface {
private $callable ;
public function __construct ( callable $value )
{
$this -> callable = $value ;
}
public function process ( QueryBuilder $queryBuilder , DataTableState $state )
{
return call_user_func ( $this -> callable , $queryBuilder , $state );
}
};
2019-11-16 19:23:45 +01:00
}
2020-01-16 21:42:29 +01:00
throw new InvalidConfigurationException ( 'Provider must be a callable or implement QueryBuilderProcessorInterface' );
2019-11-16 19:23:45 +01:00
}
2020-01-04 20:24:09 +01:00
}