1+ <?php
2+ /*
3+ * Symfony DataTables Bundle
4+ * (c) Jan Böhmer 2020
5+ *
6+ * For the full copyright and license information, please view the LICENSE
7+ * file that was distributed with this source code.
8+ */
9+
10+ declare (strict_types=1 );
11+
12+ namespace Omines \DataTablesBundle \Adapter \Doctrine ;
13+
14+ use Doctrine \ORM \Query ;
15+ use Doctrine \ORM \QueryBuilder ;
16+ use Doctrine \ORM \Tools \Pagination \Paginator ;
17+ use Omines \DataTablesBundle \Adapter \AdapterQuery ;
18+ use Omines \DataTablesBundle \Adapter \Doctrine \Event \ORMAdapterQueryEvent ;
19+ use Omines \DataTablesBundle \Adapter \Doctrine \ORMAdapterEvents ;
20+ use Omines \DataTablesBundle \Column \AbstractColumn ;
21+ use Symfony \Component \OptionsResolver \OptionsResolver ;
22+
23+ /**
24+ * Similar to ORMAdapter this class allows to access objects from the doctrine ORM.
25+ * Unlike the default ORMAdapter supports Fetch Joins (additional entites are fetched from DB via joins) using
26+ * the Doctrine Paginator.
27+ * @author Jan Böhmer
28+ */
29+ class FetchJoinORMAdapter extends ORMAdapter
30+ {
31+ protected $ use_simple_total ;
32+
33+ public function configure (array $ options )
34+ {
35+ parent ::configure ($ options );
36+ $ this ->use_simple_total = $ options ['simple_total_query ' ];
37+ }
38+
39+ protected function configureOptions (OptionsResolver $ resolver )
40+ {
41+ parent ::configureOptions ($ resolver );
42+
43+ //Enforce object hydration mode (fetch join only works for objects)
44+ $ resolver ->addAllowedValues ('hydrate ' , Query::HYDRATE_OBJECT );
45+
46+ /**
47+ * Add the possibility to replace the query for total entity count through a very simple one, to improve performance.
48+ * You can only use this option, if you did not apply any criteria to your total count.
49+ */
50+ $ resolver ->setDefault ('simple_total_query ' , false );
51+
52+ return $ resolver ;
53+ }
54+
55+ protected function prepareQuery (AdapterQuery $ query )
56+ {
57+ $ state = $ query ->getState ();
58+ $ query ->set ('qb ' , $ builder = $ this ->createQueryBuilder ($ state ));
59+ $ query ->set ('rootAlias ' , $ rootAlias = $ builder ->getDQLPart ('from ' )[0 ]->getAlias ());
60+
61+ // Provide default field mappings if needed
62+ foreach ($ state ->getDataTable ()->getColumns () as $ column ) {
63+ if (null === $ column ->getField () && isset ($ this ->metadata ->fieldMappings [$ name = $ column ->getName ()])) {
64+ $ column ->setOption ('field ' , "{$ rootAlias }. {$ name }" );
65+ }
66+ }
67+
68+ /** @var Query\Expr\From $fromClause */
69+ $ fromClause = $ builder ->getDQLPart ('from ' )[0 ];
70+ $ identifier = "{$ fromClause ->getAlias ()}. {$ this ->metadata ->getSingleIdentifierFieldName ()}" ;
71+
72+ //Use simpler (faster) total count query if the user wanted so...
73+ if ($ this ->use_simple_total ) {
74+ $ query ->setTotalRows ($ this ->getSimpleTotalCount ($ builder ));
75+ } else {
76+ $ query ->setTotalRows ($ this ->getCount ($ builder , $ identifier ));
77+ }
78+
79+ // Get record count after filtering
80+ $ this ->buildCriteria ($ builder , $ state );
81+ $ query ->setFilteredRows ($ this ->getCount ($ builder , $ identifier ));
82+
83+ // Perform mapping of all referred fields and implied fields
84+ $ aliases = $ this ->getAliases ($ query );
85+ $ query ->set ('aliases ' , $ aliases );
86+ $ query ->setIdentifierPropertyPath ($ this ->mapFieldToPropertyPath ($ identifier , $ aliases ));
87+ }
88+
89+ public function getResults (AdapterQuery $ query ): \Traversable
90+ {
91+ $ builder = $ query ->get ('qb ' );
92+ $ state = $ query ->getState ();
93+
94+ // Apply definitive view state for current 'page' of the table
95+ foreach ($ state ->getOrderBy () as list ($ column , $ direction )) {
96+ /** @var AbstractColumn $column */
97+ if ($ column ->isOrderable ()) {
98+ $ builder ->addOrderBy ($ column ->getOrderField (), $ direction );
99+ }
100+ }
101+ if ($ state ->getLength () > 0 ) {
102+ $ builder
103+ ->setFirstResult ($ state ->getStart ())
104+ ->setMaxResults ($ state ->getLength ());
105+ }
106+
107+ $ query = $ builder ->getQuery ();
108+ $ event = new ORMAdapterQueryEvent ($ query );
109+ $ state ->getDataTable ()->getEventDispatcher ()->dispatch ($ event , ORMAdapterEvents::PRE_QUERY );
110+
111+ //Use Doctrine paginator for result iteration
112+ $ paginator = new Paginator ($ query );
113+
114+ foreach ($ paginator ->getIterator () as $ result ) {
115+ yield $ result ;
116+ $ this ->manager ->detach ($ result );
117+ }
118+ }
119+
120+ public function getCount (QueryBuilder $ queryBuilder , $ identifier )
121+ {
122+ $ paginator = new Paginator ($ queryBuilder );
123+ return $ paginator ->count ();
124+ }
125+
126+ protected function getSimpleTotalCount (QueryBuilder $ queryBuilder )
127+ {
128+ /** The paginator count queries can be rather slow, so when query for total count (100ms or longer),
129+ * just return the entity count.
130+ */
131+ /** @var Query\Expr\From $from_expr */
132+ $ from_expr = $ queryBuilder ->getDQLPart ('from ' )[0 ];
133+ return $ this ->manager ->getRepository ($ from_expr ->getFrom ())->count ([]);
134+ }
135+ }
0 commit comments