Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like FieldEntry_Relationship often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use FieldEntry_Relationship, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
20 | class FieldEntry_Relationship extends FieldRelationship |
||
21 | { |
||
22 | /** |
||
23 | * |
||
24 | * Name of the field table |
||
25 | * @var string |
||
26 | */ |
||
27 | const FIELD_TBL_NAME = 'tbl_fields_entry_relationship'; |
||
28 | |||
29 | /** |
||
30 | * |
||
31 | * Current recursive level of output |
||
32 | * @var int |
||
33 | */ |
||
34 | protected $recursiveLevel = 1; |
||
35 | public function getRecursiveLevel() |
||
43 | |||
44 | /** |
||
45 | * |
||
46 | * Parent's maximum recursive level of output |
||
47 | * @var int |
||
48 | */ |
||
49 | protected $recursiveDeepness = null; |
||
50 | public function getRecursiveDeepness() |
||
58 | |||
59 | // cache managers |
||
60 | private $sectionManager; |
||
61 | private $entryManager; |
||
62 | private $sectionInfos; |
||
63 | |||
64 | public $expandIncludableElements = true; |
||
65 | |||
66 | /** |
||
67 | * |
||
68 | * Constructor for the Entry_Relationship Field object |
||
69 | */ |
||
70 | public function __construct() |
||
71 | { |
||
72 | // call the parent constructor |
||
73 | parent::__construct(); |
||
74 | // EQFA |
||
75 | $this->entryQueryFieldAdapter = new EntryQueryEntryrelationshipAdapter($this); |
||
76 | // set the name of the field |
||
77 | $this->_name = __('Entry Relationship'); |
||
78 | // permits to make it required |
||
79 | $this->_required = true; |
||
80 | // permits the make it show in the table columns |
||
81 | $this->_showcolumn = true; |
||
82 | // permits association |
||
83 | $this->_showassociation = true; |
||
84 | // current recursive level |
||
85 | $this->recursiveLevel = 1; |
||
86 | // parent's maximum recursive level of output |
||
87 | $this->recursiveDeepness = null; |
||
88 | // set as orderable |
||
89 | $this->orderable = true; |
||
90 | // set as not required by default |
||
91 | $this->set('required', 'no'); |
||
92 | // show association by default |
||
93 | $this->set('show_association', 'yes'); |
||
94 | // no sections |
||
95 | $this->set('sections', null); |
||
96 | // no max deepness |
||
97 | $this->set('deepness', null); |
||
98 | // no included elements |
||
99 | $this->set('elements', null); |
||
100 | // no modes |
||
101 | $this->set('mode', null); |
||
102 | $this->set('mode_table', null); |
||
103 | $this->set('mode_header', null); |
||
104 | $this->set('mode_footer', null); |
||
105 | // no limit |
||
106 | $this->set('min_entries', null); |
||
107 | $this->set('max_entries', null); |
||
108 | // all permissions |
||
109 | $this->set('allow_new', 'yes'); |
||
110 | $this->set('allow_edit', 'yes'); |
||
111 | $this->set('allow_link', 'yes'); |
||
112 | $this->set('allow_delete', 'no'); |
||
113 | // display options |
||
114 | $this->set('allow_collapse', 'yes'); |
||
115 | $this->set('allow_search', 'no'); |
||
116 | $this->set('show_header', 'yes'); |
||
117 | // cache managers |
||
118 | $this->sectionManager = new SectionManager; |
||
119 | $this->entryManager = new EntryManager; |
||
120 | $this->fieldManager = new FieldManager; |
||
121 | $this->sectionInfos = new SectionsInfos; |
||
122 | } |
||
123 | |||
124 | public function isSortable() |
||
125 | { |
||
126 | return false; |
||
127 | } |
||
128 | |||
129 | public function canFilter() |
||
130 | { |
||
131 | return true; |
||
132 | } |
||
133 | |||
134 | public function canPublishFilter() |
||
135 | { |
||
136 | return true; |
||
137 | } |
||
138 | |||
139 | public function canImport() |
||
140 | { |
||
141 | return false; |
||
142 | } |
||
143 | |||
144 | public function canPrePopulate() |
||
145 | { |
||
146 | return true; |
||
147 | } |
||
148 | |||
149 | public function mustBeUnique() |
||
150 | { |
||
151 | return false; |
||
152 | } |
||
153 | |||
154 | public function allowDatasourceOutputGrouping() |
||
155 | { |
||
156 | return false; |
||
157 | } |
||
158 | |||
159 | public function requiresSQLGrouping() |
||
160 | { |
||
161 | return false; |
||
162 | } |
||
163 | |||
164 | public function allowDatasourceParamOutput() |
||
165 | { |
||
166 | return true; |
||
167 | } |
||
168 | |||
169 | /* ********** INPUT AND FIELD *********** */ |
||
170 | |||
171 | |||
172 | /** |
||
173 | * |
||
174 | * Validates input |
||
175 | * Called before <code>processRawFieldData</code> |
||
176 | * @param $data |
||
177 | * @param $message |
||
178 | * @param $entry_id |
||
179 | */ |
||
180 | public function checkPostFieldData($data, &$message, $entry_id = null) |
||
181 | { |
||
182 | $message = null; |
||
183 | $required = $this->isRequired(); |
||
184 | |||
185 | if ($required && (!is_array($data) || count($data) == 0 || strlen($data['entries']) < 1)) { |
||
186 | $message = __("'%s' is a required field.", array($this->get('label'))); |
||
187 | return self::__MISSING_FIELDS__; |
||
188 | } |
||
189 | |||
190 | $entries = $data['entries']; |
||
191 | |||
192 | if (!is_array($entries)) { |
||
193 | $entries = static::getEntries($data); |
||
194 | } |
||
195 | |||
196 | // enforce limits only if required or it contains data |
||
197 | if ($required || count($entries) > 0) { |
||
198 | if ($this->getInt('min_entries') > 0 && $this->getInt('min_entries') > count($entries)) { |
||
199 | $message = __("'%s' requires a minimum of %s entries.", array($this->get('label'), $this->getInt('min_entries'))); |
||
200 | return self::__INVALID_FIELDS__; |
||
201 | } else if ($this->getInt('max_entries') > 0 && $this->getInt('max_entries') < count($entries)) { |
||
202 | $message = __("'%s' can not contains more than %s entries.", array($this->get('label'), $this->getInt('max_entries'))); |
||
203 | return self::__INVALID_FIELDS__; |
||
204 | } |
||
205 | } |
||
206 | |||
207 | return self::__OK__; |
||
208 | } |
||
209 | |||
210 | |||
211 | /** |
||
212 | * |
||
213 | * Process data before saving into database. |
||
214 | * |
||
215 | * @param array $data |
||
216 | * @param int $status |
||
217 | * @param boolean $simulate |
||
218 | * @param int $entry_id |
||
219 | * |
||
220 | * @return Array - data to be inserted into DB |
||
221 | */ |
||
222 | public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null) |
||
223 | { |
||
224 | $status = self::__OK__; |
||
225 | $entries = null; |
||
226 | |||
227 | if (!is_array($data) && !is_string($data)) { |
||
228 | return null; |
||
229 | } |
||
230 | |||
231 | if (isset($data['entries'])) { |
||
232 | $entries = $data['entries']; |
||
233 | } |
||
234 | else if (is_string($data)) { |
||
235 | $entries = $data; |
||
236 | } |
||
237 | |||
238 | $row = array( |
||
239 | 'entries' => empty($entries) ? null : $entries |
||
240 | ); |
||
241 | |||
242 | // return row |
||
243 | return $row; |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * This function permits parsing different field settings values |
||
248 | * |
||
249 | * @param array $settings |
||
250 | * the data array to initialize if necessary. |
||
251 | */ |
||
252 | public function setFromPOST(Array $settings = array()) |
||
253 | { |
||
254 | // call the default behavior |
||
255 | parent::setFromPOST($settings); |
||
256 | |||
257 | // declare a new setting array |
||
258 | $new_settings = array(); |
||
259 | |||
260 | // set new settings |
||
261 | $new_settings['sections'] = is_array($settings['sections']) ? |
||
262 | implode(self::SEPARATOR, $settings['sections']) : |
||
263 | (is_string($settings['sections']) ? $settings['sections'] : null); |
||
264 | |||
265 | $new_settings['show_association'] = $settings['show_association'] == 'yes' ? 'yes' : 'no'; |
||
266 | $new_settings['deepness'] = General::intval($settings['deepness']); |
||
267 | $new_settings['deepness'] = $new_settings['deepness'] < 1 ? null : $new_settings['deepness']; |
||
268 | $new_settings['elements'] = empty($settings['elements']) ? null : $settings['elements']; |
||
269 | $new_settings['mode'] = empty($settings['mode']) ? null : $settings['mode']; |
||
270 | $new_settings['mode_table'] = empty($settings['mode_table']) ? null : $settings['mode_table']; |
||
271 | $new_settings['mode_header'] = empty($settings['mode_header']) ? null : $settings['mode_header']; |
||
272 | $new_settings['mode_footer'] = empty($settings['mode_footer']) ? null : $settings['mode_footer']; |
||
273 | $new_settings['allow_new'] = $settings['allow_new'] == 'yes' ? 'yes' : 'no'; |
||
274 | $new_settings['allow_edit'] = $settings['allow_edit'] == 'yes' ? 'yes' : 'no'; |
||
275 | $new_settings['allow_link'] = $settings['allow_link'] == 'yes' ? 'yes' : 'no'; |
||
276 | $new_settings['allow_delete'] = $settings['allow_delete'] == 'yes' ? 'yes' : 'no'; |
||
277 | $new_settings['allow_collapse'] = $settings['allow_collapse'] == 'yes' ? 'yes' : 'no'; |
||
278 | $new_settings['allow_search'] = $settings['allow_search'] == 'yes' ? 'yes' : 'no'; |
||
279 | $new_settings['show_header'] = $settings['show_header'] == 'yes' ? 'yes' : 'no'; |
||
280 | |||
281 | // save it into the array |
||
282 | $this->setArray($new_settings); |
||
283 | } |
||
284 | |||
285 | |||
286 | /** |
||
287 | * |
||
288 | * Validates the field settings before saving it into the field's table |
||
289 | */ |
||
290 | public function checkFields(Array &$errors, $checkForDuplicates = true) |
||
291 | { |
||
292 | $parent = parent::checkFields($errors, $checkForDuplicates); |
||
293 | if ($parent != self::__OK__) { |
||
294 | return $parent; |
||
295 | } |
||
296 | |||
297 | $sections = $this->get('sections'); |
||
298 | |||
299 | if (empty($sections)) { |
||
300 | $errors['sections'] = __('At least one section must be chosen'); |
||
301 | } |
||
302 | |||
303 | return (!empty($errors) ? self::__ERROR__ : self::__OK__); |
||
304 | } |
||
305 | |||
306 | /** |
||
307 | * |
||
308 | * Save field settings into the field's table |
||
309 | */ |
||
310 | public function commit() |
||
311 | { |
||
312 | // if the default implementation works... |
||
313 | if(!parent::commit()) return false; |
||
314 | |||
315 | $id = $this->get('id'); |
||
316 | |||
317 | // exit if there is no id |
||
318 | if($id == false) return false; |
||
319 | |||
320 | // we are the child, with multiple parents |
||
321 | $child_field_id = $id; |
||
322 | |||
323 | // delete associations, only where we are the child |
||
324 | self::removeSectionAssociation($child_field_id); |
||
325 | |||
326 | $sections = $this->getSelectedSectionsArray(); |
||
327 | |||
328 | foreach ($sections as $key => $sectionId) { |
||
329 | if (empty($sectionId)) { |
||
330 | continue; |
||
331 | } |
||
332 | $parent_section_id = General::intval($sectionId); |
||
333 | if ($parent_section_id < 1) { |
||
334 | // section not found, bail out |
||
335 | continue; |
||
336 | } |
||
337 | $parent_section = $this->sectionManager |
||
338 | ->select() |
||
339 | ->section($parent_section_id) |
||
340 | ->execute() |
||
341 | ->next(); |
||
342 | if (!$parent_section) { |
||
343 | // section not found, bail out |
||
344 | continue; |
||
345 | } |
||
346 | $fields = $parent_section->fetchVisibleColumns(); |
||
347 | if (empty($fields)) { |
||
348 | // no visible field, revert to all |
||
349 | $fields = $parent_section->fetchFields(); |
||
350 | } |
||
351 | if (empty($fields)) { |
||
352 | // no fields found, bail out |
||
353 | continue; |
||
354 | } |
||
355 | $parent_field_id = current($fields)->get('id'); |
||
356 | // create association |
||
357 | SectionManager::createSectionAssociation( |
||
358 | $parent_section_id, |
||
359 | $child_field_id, |
||
360 | $parent_field_id, |
||
361 | $this->get('show_association') == 'yes' |
||
362 | ); |
||
363 | } |
||
364 | |||
365 | // declare an array contains the field's settings |
||
366 | $settings = array( |
||
367 | 'sections' => $this->get('sections'), |
||
368 | 'show_association' => $this->get('show_association'), |
||
369 | 'deepness' => $this->get('deepness'), |
||
370 | 'elements' => $this->get('elements'), |
||
371 | 'mode' => $this->get('mode'), |
||
372 | 'mode_table' => $this->get('mode_table'), |
||
373 | 'mode_header' => $this->get('mode_header'), |
||
374 | 'mode_footer' => $this->get('mode_footer'), |
||
375 | 'min_entries' => $this->get('min_entries'), |
||
376 | 'max_entries' => $this->get('max_entries'), |
||
377 | 'allow_new' => $this->get('allow_new'), |
||
378 | 'allow_edit' => $this->get('allow_edit'), |
||
379 | 'allow_link' => $this->get('allow_link'), |
||
380 | 'allow_delete' => $this->get('allow_delete'), |
||
381 | 'allow_collapse' => $this->get('allow_collapse'), |
||
382 | 'allow_search' => $this->get('allow_search'), |
||
383 | 'show_header' => $this->get('show_header'), |
||
384 | ); |
||
385 | |||
386 | return FieldManager::saveSettings($id, $settings); |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * |
||
391 | * This function allows Fields to cleanup any additional things before it is removed |
||
392 | * from the section. |
||
393 | * @return boolean |
||
394 | */ |
||
395 | public function tearDown() |
||
396 | { |
||
397 | self::removeSectionAssociation($this->get('id')); |
||
398 | return parent::tearDown(); |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * Generates the where filter for searching by entry id |
||
403 | * |
||
404 | * @param string $value |
||
405 | * @param @optional string $col |
||
406 | */ |
||
407 | public function generateWhereFilter($value, $col = 'd') |
||
408 | { |
||
409 | $junction = $andOperation ? 'and' : 'or'; |
||
|
|||
410 | |||
411 | if (!$value) { |
||
412 | return [ |
||
413 | $junction => [ |
||
414 | [$col . '.entries' => null], |
||
415 | ], |
||
416 | ]; |
||
417 | } |
||
418 | |||
419 | return [ |
||
420 | 'or' => [ |
||
421 | [$col . '.entries' => $value], |
||
422 | [$col . '.entries' => ['like' => $value . ',%']], |
||
423 | [$col . '.entries' => ['like' => '%,' . $value]], |
||
424 | [$col . '.entries' => ['like' => '%,' . $value . ',%']], |
||
425 | ] |
||
426 | ]; |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Fetch the number of associated entries for a particular entry id |
||
431 | * |
||
432 | * @param string $value |
||
433 | */ |
||
434 | public function fetchAssociatedEntryCount($value) |
||
435 | { |
||
436 | if (!$value) { |
||
437 | return 0; |
||
438 | } |
||
439 | |||
440 | $where = $this->generateWhereFilter($value); |
||
441 | |||
442 | $entries = Symphony::Database() |
||
443 | ->select(['e.id']) |
||
444 | ->from('tbl_entries', 'e') |
||
445 | ->innerJoin('tbl_entries_data_' . $this->get('id'), 'd') |
||
446 | ->on(['e.id' => '$d.entry_id']) |
||
447 | ->where(['e.section_id' => $this->get('parent_section')]) |
||
448 | ->where($where) |
||
449 | ->execute() |
||
450 | ->rows(); |
||
451 | |||
452 | return count($entries); |
||
453 | } |
||
454 | |||
455 | public function fetchAssociatedEntrySearchValue($data, $field_id = null, $parent_entry_id = null) |
||
456 | { |
||
457 | return $parent_entry_id; |
||
458 | } |
||
459 | |||
460 | public function findRelatedEntries($entry_id, $parent_field_id) |
||
461 | { |
||
462 | $entries = $this->entryManager |
||
463 | ->select() |
||
464 | ->section($this->get('parent_section')) |
||
465 | ->filter($this->get('id'), [(string)$entry_id]) |
||
466 | ->schema(['id']) |
||
467 | ->execute() |
||
468 | ->rows(); |
||
469 | |||
470 | return array_map(function ($e) { |
||
471 | return $e['id']; |
||
472 | }, $entries); |
||
473 | } |
||
474 | |||
475 | public function findParentRelatedEntries($parent_field_id, $entry_id) |
||
476 | { |
||
477 | $entry = $this->entryManager |
||
478 | ->select() |
||
479 | ->entry($entry_id) |
||
480 | ->section($this->entryManager->fetchEntrySectionID($entry_id)) |
||
481 | ->includeAllFields() |
||
482 | ->schema(array($this->get('label'))) |
||
483 | ->execute() |
||
484 | ->next(); |
||
485 | |||
486 | if (!$entry) { |
||
487 | return array(); |
||
488 | } |
||
489 | |||
490 | return self::getEntries($entry->getData($this->get('id'))); |
||
491 | } |
||
492 | |||
493 | public function fetchFilterableOperators() |
||
494 | { |
||
495 | return array( |
||
496 | array( |
||
497 | 'title' => 'links to', |
||
498 | 'filter' => ' ', |
||
499 | 'help' => __('Find entries that links to the specified filter') |
||
500 | ), |
||
501 | array( |
||
502 | 'filter' => 'sql: NOT NULL', |
||
503 | 'title' => 'is not empty', |
||
504 | 'help' => __('Find entries with a non-empty value.') |
||
505 | ), |
||
506 | array( |
||
507 | 'filter' => 'sql: NULL', |
||
508 | 'title' => 'is empty', |
||
509 | 'help' => __('Find entries with an empty value.') |
||
510 | ), |
||
511 | array( |
||
512 | 'title' => 'contains', |
||
513 | 'filter' => 'regexp: ', |
||
514 | 'help' => __('Find values that match the given <a href="%s">MySQL regular expressions</a>.', array( |
||
515 | 'https://dev.mysql.com/doc/mysql/en/regexp.html' |
||
516 | )) |
||
517 | ), |
||
518 | array( |
||
519 | 'title' => 'does not contain', |
||
520 | 'filter' => 'not-regexp: ', |
||
521 | 'help' => __('Find values that do not match the given <a href="%s">MySQL regular expressions</a>.', array( |
||
522 | 'https://dev.mysql.com/doc/mysql/en/regexp.html' |
||
523 | )) |
||
524 | ), |
||
525 | ); |
||
526 | } |
||
527 | |||
528 | public function fetchSuggestionTypes() |
||
529 | { |
||
530 | return array('association'); |
||
531 | } |
||
532 | |||
533 | public function fetchIDsfromValue($value) |
||
534 | { |
||
535 | $ids = array(); |
||
536 | $sections = $this->getArray('sections'); |
||
537 | |||
538 | foreach ($sections as $sectionId) { |
||
539 | $section = $this->sectionManager |
||
540 | ->select() |
||
541 | ->section($sectionId) |
||
542 | ->execute() |
||
543 | ->next(); |
||
544 | |||
545 | if (!$section) { |
||
546 | continue; |
||
547 | } |
||
548 | $filterableFields = $section->fetchFilterableFields(); |
||
549 | if (empty($filterableFields)) { |
||
550 | continue; |
||
551 | } |
||
552 | foreach ($filterableFields as $fId => $field) { |
||
553 | if ($field instanceof FieldRelationship) { |
||
554 | continue; |
||
555 | } |
||
556 | $fEntries = (new EntryManager) |
||
557 | ->select(['e.id']) |
||
558 | ->section($sectionId) |
||
559 | ->filter($fId, [$value]) |
||
560 | ->execute() |
||
561 | ->rows(); |
||
562 | |||
563 | if (!empty($fEntries)) { |
||
564 | $ids = array_merge($ids, $fEntries); |
||
565 | break; |
||
566 | } |
||
567 | } |
||
568 | } |
||
569 | |||
570 | return array_map(function ($e) { |
||
571 | return $e['id']; |
||
572 | }, $ids); |
||
573 | } |
||
574 | |||
575 | public function prepareAssociationsDrawerXMLElement(Entry $e, array $parent_association, $prepolutate = '') |
||
576 | { |
||
577 | $currentSection = $this->sectionManager |
||
578 | ->select() |
||
579 | ->section($parent_association['child_section_id']) |
||
580 | ->execute() |
||
581 | ->next(); |
||
582 | $visibleCols = $currentSection->fetchVisibleColumns(); |
||
583 | $outputFieldId = current($visibleCols)->get('id'); |
||
584 | $outputField = $this->fieldManager |
||
585 | ->select() |
||
586 | ->field($outputFieldId) |
||
587 | ->execute() |
||
588 | ->next(); |
||
589 | |||
590 | $value = $outputField->prepareReadableValue($e->getData($outputFieldId), $e->get('id'), true, __('None')); |
||
591 | |||
592 | $li = new XMLElement('li'); |
||
593 | $li->setAttribute('class', 'field-' . $this->get('type')); |
||
594 | $a = new XMLElement('a', strip_tags($value)); |
||
595 | $a->setAttribute('href', SYMPHONY_URL . '/publish/' . $parent_association['handle'] . '/edit/' . $e->get('id') . '/'); |
||
596 | $li->appendChild($a); |
||
597 | |||
598 | return $li; |
||
599 | } |
||
600 | |||
601 | /* ******* EVENTS ******* */ |
||
602 | |||
603 | public function getExampleFormMarkup() |
||
604 | { |
||
605 | $label = Widget::Label($this->get('label')); |
||
606 | $label->appendChild(Widget::Input('fields['.$this->get('element_name').'][entries]', null, 'hidden')); |
||
607 | |||
608 | return $label; |
||
609 | } |
||
610 | |||
611 | |||
612 | /* ******* DATA SOURCE ******* */ |
||
613 | |||
614 | private function fetchEntryWithData($eId, $sectionId, $elements = array()) |
||
615 | { |
||
616 | $q = $this->entryManager |
||
617 | ->select() |
||
618 | ->entry($eId) |
||
619 | ->section($sectionId); |
||
620 | if ($elements === null) { |
||
621 | $q->includeAllFields(); |
||
622 | } else { |
||
623 | $q->schema($elements); |
||
624 | } |
||
625 | return $q->execute() |
||
626 | ->next(); |
||
627 | } |
||
628 | |||
629 | private function fetchEntryOnly($eId) |
||
630 | { |
||
631 | return $this->entryManager |
||
632 | ->select() |
||
633 | ->entry($eId) |
||
634 | ->execute() |
||
635 | ->next(); |
||
636 | } |
||
637 | |||
638 | protected function fetchAllIncludableElements() |
||
639 | { |
||
640 | $sections = $this->getArray('sections'); |
||
641 | return $allElements = array_reduce($this->sectionInfos->fetch($sections), function ($memo, $item) { |
||
642 | return array_merge($memo, array_map(function ($field) use ($item) { |
||
643 | return $item['handle'] . '.' . $field['handle']; |
||
644 | }, $item['fields'])); |
||
645 | }, array()); |
||
646 | } |
||
647 | |||
648 | public function fetchIncludableElements() |
||
649 | { |
||
650 | $label = $this->get('element_name'); |
||
651 | $elements = $this->getArray('elements'); |
||
652 | if (empty($elements)) { |
||
653 | $elements = array('*'); |
||
654 | } |
||
655 | $includedElements = array(); |
||
656 | // Include everything |
||
657 | if ($this->expandIncludableElements) { |
||
658 | $includedElements[] = $label . ': *'; |
||
659 | } |
||
660 | // Include individual elements |
||
661 | foreach ($elements as $elem) { |
||
662 | $elem = trim($elem); |
||
663 | if ($elem !== '*') { |
||
664 | $includedElements[] = $label . ': ' . $elem; |
||
665 | } else if ($this->expandIncludableElements) { |
||
666 | $includedElements = array_unique(array_merge($includedElements, array_map(function ($item) use ($label) { |
||
667 | return $label . ': ' . $item; |
||
668 | }, $this->fetchAllIncludableElements()))); |
||
669 | } else { |
||
670 | $includedElements = array('*'); |
||
671 | } |
||
672 | } |
||
673 | return $includedElements; |
||
674 | } |
||
675 | |||
676 | /** |
||
677 | * Appends data into the XML tree of a Data Source |
||
678 | * @param $wrapper |
||
679 | * @param $data |
||
680 | */ |
||
681 | public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null) |
||
682 | { |
||
683 | if (!is_array($data) || empty($data)) { |
||
684 | return; |
||
685 | } |
||
686 | |||
687 | // try to find an existing root |
||
688 | $root = null; |
||
689 | $newRoot = false; |
||
690 | foreach (array_reverse($wrapper->getChildren()) as $xmlField) { |
||
691 | if ($xmlField->getName() === $this->get('element_name')) { |
||
692 | $root = $xmlField; |
||
693 | break; |
||
694 | } |
||
695 | } |
||
696 | |||
697 | // root was not found, create one |
||
698 | if (!$root) { |
||
699 | $root = new XMLElement($this->get('element_name')); |
||
700 | $newRoot = true; |
||
701 | } |
||
702 | |||
703 | // devkit will load |
||
704 | $devkit = isset($_GET['debug']) && (empty($_GET['debug']) || $_GET['debug'] == 'xml'); |
||
705 | |||
706 | // selected items |
||
707 | $entries = static::getEntries($data); |
||
708 | |||
709 | // current linked entries |
||
710 | $root->setAttribute('entries', $data['entries']); |
||
711 | |||
712 | // available sections |
||
713 | $root->setAttribute('sections', $this->get('sections')); |
||
714 | |||
715 | // included elements |
||
716 | $elements = static::parseElements($this); |
||
717 | |||
718 | // DS mode |
||
719 | if (!$mode) { |
||
720 | $mode = '*'; |
||
721 | } |
||
722 | |||
723 | $parentDeepness = General::intval($this->recursiveDeepness); |
||
724 | $deepness = General::intval($this->get('deepness')); |
||
725 | |||
726 | // both deepnesses are defined and parent restricts more |
||
727 | if ($parentDeepness > 0 && $deepness > 0 && $parentDeepness < $deepness) { |
||
728 | $deepness = $parentDeepness; |
||
729 | } |
||
730 | // parent is defined, current is not |
||
731 | else if ($parentDeepness > 0 && $deepness < 1) { |
||
732 | $deepness = $parentDeepness; |
||
733 | } |
||
734 | |||
735 | // cache recursive level because recursion might |
||
736 | // change its value later on. |
||
737 | $recursiveLevel = $this->recursiveLevel; |
||
738 | |||
739 | // build entries |
||
740 | foreach ($entries as $eId) { |
||
741 | // try to find and existing item |
||
742 | // TODO: keep last index found since it should be the next |
||
743 | $item = null; |
||
744 | $newItem = false; |
||
745 | foreach ($root->getChildren() as $xmlItem) { |
||
746 | if (General::intval($xmlItem->getAttribute('id')) === General::intval($eId)) { |
||
747 | $item = $xmlItem; |
||
748 | break; |
||
749 | } |
||
750 | } |
||
751 | |||
752 | // item was not found, create one |
||
753 | if (!$item) { |
||
754 | $item = new XMLElement('item'); |
||
755 | $item->setAllowEmptyAttributes(false); |
||
756 | // output id |
||
757 | $item->setAttribute('id', $eId); |
||
758 | // output recursive level |
||
759 | $item->setAttribute('level', $recursiveLevel); |
||
760 | $item->setAttribute('max-level', $deepness); |
||
761 | $newItem = true; |
||
762 | // item was found, but it is an error, so we can skip it |
||
763 | } else if ($item->getName() === 'error') { |
||
764 | continue; |
||
765 | } |
||
766 | |||
767 | // max recursion check |
||
768 | if ($deepness < 1 || $recursiveLevel < $deepness) { |
||
769 | // current entry, without data |
||
770 | $entry = $this->fetchEntryOnly($eId); |
||
771 | |||
772 | // entry not found... |
||
773 | if (!$entry || empty($entry)) { |
||
774 | $error = new XMLElement('error'); |
||
775 | $error->setAttribute('id', $eId); |
||
776 | $error->setValue(__('Error: entry `%s` not found', array($eId))); |
||
777 | $root->prependChild($error); |
||
778 | continue; |
||
779 | } |
||
780 | |||
781 | // fetch section infos |
||
782 | $sectionId = $entry->get('section_id'); |
||
783 | $section = $this->sectionManager |
||
784 | ->select() |
||
785 | ->section($sectionId) |
||
786 | ->execute() |
||
787 | ->next(); |
||
788 | $sectionName = $section->get('handle'); |
||
789 | |||
790 | // set section related attributes |
||
791 | $item->setAttribute('section-id', $sectionId); |
||
792 | $item->setAttribute('section', $sectionName); |
||
793 | |||
794 | // Get the valid elements for this section only |
||
795 | $validElements = $elements[$sectionName]; |
||
796 | |||
797 | // adjust the mode for the current section |
||
798 | $curMode = $mode; |
||
799 | |||
800 | // remove section name from current mode so "sectionName.field" becomes simply "field" |
||
801 | if (preg_match('/^(' . $sectionName . '\.)(.*)$/sU', $curMode)) { |
||
802 | $curMode = preg_replace('/^' . $sectionName . '\./sU', '', $curMode); |
||
803 | } |
||
804 | // remove section name from current mode "sectionName" and |
||
805 | // treat it like if it is "sectionName: *" |
||
806 | else if (preg_match('/^(' . $sectionName . ')$/sU', $curMode)) { |
||
807 | $curMode = '*'; |
||
808 | } |
||
809 | // section name was not found in mode, check if the mode is "*" |
||
810 | else if ($curMode != '*') { |
||
811 | // mode forbids this section |
||
812 | $validElements = null; |
||
813 | } |
||
814 | |||
815 | // this section is not selected, bail out |
||
816 | View Code Duplication | if (!is_array($validElements)) { |
|
817 | if ($newItem) { |
||
818 | if ($devkit) { |
||
819 | $item->setAttribute('x-forbidden-by-ds', $curMode); |
||
820 | } |
||
821 | $root->appendChild($item); |
||
822 | } |
||
823 | continue; |
||
824 | } else { |
||
825 | if ($devkit) { |
||
826 | $item->setAttribute('x-forbidden-by-ds', null); |
||
827 | } |
||
828 | } |
||
829 | |||
830 | // selected fields for fetching |
||
831 | $sectionElements = array(); |
||
832 | |||
833 | // everything is allowed |
||
834 | if (in_array('*', $validElements)) { |
||
835 | if ($curMode !== '*') { |
||
836 | // get only the mode |
||
837 | $sectionElements = array($curMode); |
||
838 | } |
||
839 | else { |
||
840 | // setting null = get all |
||
841 | $sectionElements = null; |
||
842 | } |
||
843 | } |
||
844 | // only use valid elements |
||
845 | else { |
||
846 | if ($curMode !== '*') { |
||
847 | // is this field allowed ? |
||
848 | if (self::isFieldIncluded($curMode, $validElements)) { |
||
849 | // get only the mode |
||
850 | $sectionElements = array($curMode); |
||
851 | } |
||
852 | else { |
||
853 | // $curMode selects something outside of |
||
854 | // the valid elements: select nothing |
||
855 | $sectionElements = array(); |
||
856 | } |
||
857 | } |
||
858 | else { |
||
859 | // use field's valid elements |
||
860 | $sectionElements = $validElements; |
||
861 | } |
||
862 | } |
||
863 | |||
864 | // Filtering is enabled, but nothing is selected |
||
865 | View Code Duplication | if (is_array($sectionElements) && empty($sectionElements)) { |
|
866 | if ($newItem) { |
||
867 | $root->appendChild($item); |
||
868 | if ($devkit) { |
||
869 | $item->setAttribute('x-forbidden-by-selection', $curMode); |
||
870 | } |
||
871 | } |
||
872 | continue; |
||
873 | } else { |
||
874 | if ($devkit) { |
||
875 | $item->setAttribute('x-forbidden-by-selection', null); |
||
876 | } |
||
877 | } |
||
878 | |||
879 | // fetch current entry again, but with data for the allowed schema |
||
880 | $entry = $this->fetchEntryWithData($eId, $sectionId, $sectionElements); |
||
881 | |||
882 | // cache the entry data |
||
883 | $entryData = $entry->getData(); |
||
884 | |||
885 | // for each field returned for this entry... |
||
886 | foreach ($entryData as $fieldId => $data) { |
||
887 | $filteredData = array_filter($data, function ($value) { |
||
888 | return $value != null; |
||
889 | }); |
||
890 | |||
891 | if (empty($filteredData)) { |
||
892 | continue; |
||
893 | } |
||
894 | |||
895 | $field = $this->fieldManager |
||
896 | ->select() |
||
897 | ->field($fieldId) |
||
898 | ->execute() |
||
899 | ->next(); |
||
900 | $fieldName = $field->get('element_name'); |
||
901 | $fieldCurMode = self::extractMode($fieldName, $curMode); |
||
902 | |||
903 | $parentIncludableElement = self::getSectionElementName($fieldName, $validElements); |
||
904 | $parentIncludableElementMode = self::extractMode($fieldName, $parentIncludableElement); |
||
905 | |||
906 | // Special treatments for ERF |
||
907 | if ($field instanceof FieldEntry_relationship) { |
||
908 | // Increment recursive level |
||
909 | $field->recursiveLevel = $recursiveLevel + 1; |
||
910 | $field->recursiveDeepness = $deepness; |
||
911 | } |
||
912 | |||
913 | $submodes = null; |
||
914 | // Parent mode is not defined (we are selecting the whole section) |
||
915 | if ($parentIncludableElementMode === null) { |
||
916 | if ($fieldCurMode == null) { |
||
917 | // Field does not defined a mode either: use the field's default |
||
918 | $submodes = null; |
||
919 | } else { |
||
920 | // Use the current field's mode |
||
921 | $submodes = array($fieldCurMode); |
||
922 | } |
||
923 | if ($devkit) { |
||
924 | $item->setAttribute('x-selection-mode-empty', null); |
||
925 | } |
||
926 | } else { |
||
927 | // Field mode is not defined or it is the same as the parent |
||
928 | if ($fieldCurMode === null || $fieldCurMode == $parentIncludableElementMode) { |
||
929 | if ($devkit) { |
||
930 | $item->setAttribute('x-selection-mode-empty', null); |
||
931 | } |
||
932 | // Use parent mode |
||
933 | $submodes = array($parentIncludableElementMode); |
||
934 | } else { |
||
935 | if ($devkit) { |
||
936 | $item->setAttribute('x-selection-mode-empty', 'yes'); |
||
937 | } |
||
938 | // Empty selection |
||
939 | $submodes = array(); |
||
940 | } |
||
941 | } |
||
942 | |||
943 | // current selection does not specify a mode |
||
944 | if ($submodes === null) { |
||
945 | if ($field instanceof FieldEntry_Relationship) { |
||
946 | $field->expandIncludableElements = false; |
||
947 | } |
||
948 | $submodes = array_map(function ($fieldIncludableElement) use ($fieldName) { |
||
949 | return FieldEntry_relationship::extractMode($fieldName, $fieldIncludableElement); |
||
950 | }, $field->fetchIncludableElements()); |
||
951 | if ($field instanceof FieldEntry_Relationship) { |
||
952 | $field->expandIncludableElements = true; |
||
953 | } |
||
954 | } |
||
955 | |||
956 | // Append the formatted element for each requested mode |
||
957 | foreach ($submodes as $submode) { |
||
958 | $field->appendFormattedElement($item, $data, $encode, $submode, $eId); |
||
959 | } |
||
960 | } |
||
961 | // output current mode |
||
962 | $item->setAttribute('matched-element', $curMode); |
||
963 | // no field selected |
||
964 | if (is_array($sectionElements) && empty($sectionElements)) { |
||
965 | $item->setAttribute('empty-selection', 'yes'); |
||
966 | } |
||
967 | } // end max recursion check |
||
968 | |||
969 | if ($newItem) { |
||
970 | // append item when done |
||
971 | $root->appendChild($item); |
||
972 | } |
||
973 | } // end each entries |
||
974 | |||
975 | if ($newRoot) { |
||
976 | // output mode for this field |
||
977 | if ($devkit) { |
||
978 | $root->setAttribute('x-data-source-mode', $mode); |
||
979 | $root->setAttribute('x-field-included-elements', $this->get('elements')); |
||
980 | } |
||
981 | |||
982 | // add all our data to the wrapper; |
||
983 | $wrapper->appendChild($root); |
||
984 | } else { |
||
985 | if ($devkit) { |
||
986 | $root->setAttribute('x-data-source-mode', $root->getAttribute('x-data-source-mode') . ', ' . $mode); |
||
987 | } |
||
988 | } |
||
989 | |||
990 | // clean up |
||
991 | $this->recursiveLevel = 1; |
||
992 | $this->recursiveDeepness = null; |
||
993 | } |
||
994 | |||
995 | public function getParameterPoolValue(array $data, $entry_id = null) |
||
1000 | |||
1001 | /* ********* Utils *********** */ |
||
1002 | |||
1003 | /** |
||
1004 | * Return true if $fieldName is allowed in $sectionElements |
||
1005 | * @param string $fieldName |
||
1006 | * @param string $sectionElements |
||
1007 | * @return bool |
||
1008 | */ |
||
1009 | public static function isFieldIncluded($fieldName, $sectionElements) |
||
1013 | |||
1014 | public static function getSectionElementName($fieldName, $sectionElements) |
||
1034 | |||
1035 | public static function parseElements($field) |
||
1036 | { |
||
1037 | $elements = array(); |
||
1038 | $exElements = $field->getArray('elements'); |
||
1039 | |||
1040 | |||
1041 | if (in_array('*', $exElements)) { |
||
1042 | $sections = $field->getArray('sections'); |
||
1043 | $sm = new SectionManager; |
||
1044 | $sections = $sm |
||
1045 | ->select() |
||
1046 | ->sections($sections) |
||
1047 | ->execute() |
||
1048 | ->rows(); |
||
1049 | return array_reduce($sections, function ($result, $section) { |
||
1050 | $result[$section->get('handle')] = array('*'); |
||
1051 | return $result; |
||
1052 | }, array()); |
||
1053 | } |
||
1054 | |||
1055 | foreach ($exElements as $value) { |
||
1056 | if (!$value) { |
||
1057 | continue; |
||
1058 | } |
||
1059 | // sectionName.fieldName or sectionName.* |
||
1060 | $parts = array_map('trim', explode('.', $value, 2)); |
||
1061 | // first time seeing this section |
||
1062 | if (!isset($elements[$parts[0]])) { |
||
1063 | $elements[$parts[0]] = array(); |
||
1064 | } |
||
1065 | // we have a value after the dot |
||
1077 | |||
1078 | public static function extractMode($fieldName, $mode) |
||
1090 | |||
1091 | private function buildSectionSelect($name) |
||
1109 | |||
1110 | View Code Duplication | private function appendSelectionSelect(&$wrapper) |
|
1124 | |||
1125 | private function createEntriesHiddenInput($data) |
||
1135 | |||
1136 | private function createActionBarMenu($sections) |
||
1204 | |||
1205 | /* ********* UI *********** */ |
||
1206 | |||
1207 | /** |
||
1208 | * |
||
1209 | * Builds the UI for the field's settings when creating/editing a section |
||
1210 | * @param XMLElement $wrapper |
||
1211 | * @param array $errors |
||
1212 | */ |
||
1213 | public function displaySettingsPanel(XMLElement &$wrapper, $errors = null) |
||
1345 | |||
1346 | /** |
||
1347 | * |
||
1348 | * Builds the UI for the publish page |
||
1349 | * @param XMLElement $wrapper |
||
1350 | * @param mixed $data |
||
1351 | * @param mixed $flagWithError |
||
1352 | * @param string $fieldnamePrefix |
||
1353 | * @param string $fieldnamePostfix |
||
1354 | */ |
||
1355 | public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null) |
||
1417 | |||
1418 | /** |
||
1419 | * |
||
1420 | * Return a plain text representation of the field's data |
||
1421 | * @param array $data |
||
1422 | * @param int $entry_id |
||
1423 | */ |
||
1424 | public function prepareTextValue($data, $entry_id = null) |
||
1431 | |||
1432 | /** |
||
1433 | * Format this field value for display as readable text value. |
||
1434 | * |
||
1435 | * @param array $data |
||
1436 | * an associative array of data for this string. At minimum this requires a |
||
1437 | * key of 'value'. |
||
1438 | * @param integer $entry_id (optional) |
||
1439 | * An option entry ID for more intelligent processing. Defaults to null. |
||
1440 | * @param string $defaultValue (optional) |
||
1441 | * The value to use when no plain text representation of the field's data |
||
1442 | * can be made. Defaults to null. |
||
1443 | * @return string |
||
1444 | * the readable text summary of the values of this field instance. |
||
1445 | */ |
||
1446 | public function prepareReadableValue($data, $entry_id = null, $truncate = false, $defaultValue = 'None') |
||
1470 | |||
1471 | /** |
||
1472 | * Format this field value for display in the publish index tables. |
||
1473 | * |
||
1474 | * @param array $data |
||
1475 | * an associative array of data for this string. At minimum this requires a |
||
1476 | * key of 'value'. |
||
1477 | * @param XMLElement $link (optional) |
||
1478 | * an XML link structure to append the content of this to provided it is not |
||
1479 | * null. it defaults to null. |
||
1480 | * @param integer $entry_id (optional) |
||
1481 | * An option entry ID for more intelligent processing. defaults to null |
||
1482 | * @return string |
||
1483 | * the formatted string summary of the values of this field instance. |
||
1484 | */ |
||
1485 | public function prepareTableValue($data, XMLElement $link = null, $entry_id = null) |
||
1528 | |||
1529 | /* ********* SQL Data Definition ************* */ |
||
1530 | |||
1531 | /** |
||
1532 | * |
||
1533 | * Creates table needed for entries of individual fields |
||
1534 | */ |
||
1535 | public function createTable() |
||
1558 | |||
1559 | /** |
||
1560 | * Creates the table needed for the settings of the field |
||
1561 | */ |
||
1562 | public static function createFieldTable() |
||
1657 | |||
1658 | public static function update_102() |
||
1713 | |||
1714 | View Code Duplication | public static function update_103() |
|
1729 | |||
1730 | public static function update_200() |
||
1778 | |||
1779 | View Code Duplication | public static function update_2008() |
|
1794 | |||
1795 | /** |
||
1796 | * |
||
1797 | * Drops the table needed for the settings of the field |
||
1798 | */ |
||
1799 | public static function deleteFieldTable() |
||
1807 | |||
1808 | private static function removeSectionAssociation($child_field_id) |
||
1816 | } |
||
1817 |
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.