Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 639 lines (568 sloc) 23.068 kB
7f90fa0 Initial commit of Revision 5 code
Alistair Kearney authored
1 <?php
2
854f78a Last lot of code formatting to PSR-2
John Porter authored
3 /**
4 * @package toolkit
5 */
6 /**
7 * The `FieldManager` class is responsible for managing all fields types in Symphony.
8 * Fields are stored on the file system either in the `/fields` folder of `TOOLKIT` or
9 * in a `fields` folder in an extension directory.
10 */
11
12 class FieldManager implements FileResource
13 {
14 /**
15 * An array of all the objects that the Manager is responsible for.
16 * Defaults to an empty array.
17 * @var array
18 */
19 protected static $_pool = array();
20
21 /**
22 * An array of all fields whose have been created by ID
23 * @var array
24 */
25 private static $_initialiased_fields = array();
26
27 /**
28 * Given the filename of a Field, return it's handle. This will remove
29 * the Symphony conventions of `field.*.php`
30 *
31 * @param string $filename
32 * The filename of the Field
33 * @return string
34 */
35 public static function __getHandleFromFilename($filename)
36 {
37 return preg_replace(array('/^field./i', '/.php$/i'), '', $filename);
38 }
39
40 /**
41 * Given a type, returns the full class name of a Field. Fields use a
42 * 'field' prefix
43 *
44 * @param string $type
45 * A field handle
46 * @return string
47 */
48 public static function __getClassName($type)
49 {
50 return 'field' . $type;
51 }
52
53 /**
54 * Finds a Field by type by searching the `TOOLKIT . /fields` folder and then
55 * any fields folders in the installed extensions. The function returns
56 * the path to the folder where the field class resides.
57 *
58 * @param string $type
59 * The field handle, that is, `field.{$handle}.php`
64b19cc @brendo Additional scrutinizer fixes
brendo authored
60 * @return string|boolean
854f78a Last lot of code formatting to PSR-2
John Porter authored
61 */
62 public static function __getClassPath($type)
63 {
64 if (is_file(TOOLKIT . "/fields/field.{$type}.php")) {
65 return TOOLKIT . '/fields';
66 } else {
67 $extensions = Symphony::ExtensionManager()->listInstalledHandles();
68
69 if (is_array($extensions) && !empty($extensions)) {
70 foreach ($extensions as $e) {
71 if (is_file(EXTENSIONS . "/{$e}/fields/field.{$type}.php")) {
72 return EXTENSIONS . "/{$e}/fields";
73 }
74 }
75 }
76 }
77
78 return false;
79 }
80
81 /**
82 * Given a field type, return the path to it's class
83 *
84 * @see __getClassPath()
85 * @param string $type
86 * The handle of the field to load (it's type)
87 * @return string
88 */
89 public static function __getDriverPath($type)
90 {
91 return self::__getClassPath($type) . "/field.{$type}.php";
92 }
93
94 /**
95 * This function is not implemented by the `FieldManager` class
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
96 *
97 * @return boolean
854f78a Last lot of code formatting to PSR-2
John Porter authored
98 */
99 public static function about($name)
100 {
101 return false;
102 }
103
104 /**
105 * Given an associative array of fields, insert them into the database
106 * returning the resulting Field ID if successful, or false if there
107 * was an error. As fields are saved in order on a section, a query is
108 * made to determine the sort order of this field to be current sort order
109 * +1.
110 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
111 * @throws DatabaseException
854f78a Last lot of code formatting to PSR-2
John Porter authored
112 * @param array $fields
113 * Associative array of field names => values for the Field object
114 * @return integer|boolean
115 * Returns a Field ID of the created Field on success, false otherwise.
116 */
117 public static function add(array $fields)
118 {
119 if (!isset($fields['sortorder'])) {
120 $fields['sortorder'] = self::fetchNextSortOrder();
121 }
122
123 if (!Symphony::Database()->insert($fields, 'tbl_fields')) {
124 return false;
125 }
126
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
127 return Symphony::Database()->getInsertID();
854f78a Last lot of code formatting to PSR-2
John Porter authored
128 }
129
130 /**
131 * Save the settings for a Field given it's `$field_id` and an associative
132 * array of settings.
133 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
134 * @throws DatabaseException
854f78a Last lot of code formatting to PSR-2
John Porter authored
135 * @since Symphony 2.3
136 * @param integer $field_id
137 * The ID of the field
138 * @param array $settings
139 * An associative array of settings, where the key is the column name
140 * and the value is the value.
141 * @return boolean
142 * True on success, false on failure
143 */
144 public static function saveSettings($field_id, $settings)
145 {
146 // Get the type of this field:
147 $type = self::fetchFieldTypeFromID($field_id);
148
149 // Delete the original settings:
150 Symphony::Database()->delete("`tbl_fields_".$type."`", sprintf("`field_id` = %d LIMIT 1", $field_id));
151
152 // Insert the new settings into the type table:
153 if (!isset($settings['field_id'])) {
154 $settings['field_id'] = $field_id;
155 }
156
157 return Symphony::Database()->insert($settings, 'tbl_fields_'.$type);
158 }
159
160 /**
161 * Given a Field ID and associative array of fields, update an existing Field
162 * row in the `tbl_fields`table. Returns boolean for success/failure
163 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
164 * @throws DatabaseException
854f78a Last lot of code formatting to PSR-2
John Porter authored
165 * @param integer $id
166 * The ID of the Field that should be updated
167 * @param array $fields
168 * Associative array of field names => values for the Field object
169 * This array does need to contain every value for the field object, it
170 * can just be the changed values.
171 * @return boolean
172 */
173 public static function edit($id, array $fields)
174 {
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
175 if (!Symphony::Database()->update($fields, "tbl_fields", sprintf(" `id` = %d", $id))) {
854f78a Last lot of code formatting to PSR-2
John Porter authored
176 return false;
177 }
178
179 return true;
180 }
181
182 /**
183 * Given a Field ID, delete a Field from Symphony. This will remove the field from
184 * the fields table, all of the data stored in this field's `tbl_entries_data_$id` any
185 * existing section associations. This function additionally call the Field's `tearDown`
186 * method so that it can cleanup any additional settings or entry tables it may of created.
187 *
188 * @throws DatabaseException
189 * @throws Exception
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
190 * @param integer $id
191 * The ID of the Field that should be deleted
854f78a Last lot of code formatting to PSR-2
John Porter authored
192 * @return boolean
193 */
194 public static function delete($id)
195 {
196 $existing = self::fetch($id);
197 $existing->tearDown();
198
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
199 Symphony::Database()->delete('tbl_fields', sprintf(" `id` = %d", $id));
200 Symphony::Database()->delete('tbl_fields_'.$existing->handle(), sprintf(" `field_id` = %d", $id));
854f78a Last lot of code formatting to PSR-2
John Porter authored
201 SectionManager::removeSectionAssociation($id);
202
203 Symphony::Database()->query('DROP TABLE IF EXISTS `tbl_entries_data_'.$id.'`');
204
205 return true;
206 }
207
208 /**
209 * The fetch method returns a instance of a Field from tbl_fields. The most common
210 * use of this function is to retrieve a Field by ID, but it can be used to retrieve
211 * Fields from a Section also. There are several parameters that can be used to fetch
212 * fields by their Type, Location, by a Field Constant or with a custom WHERE query.
213 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
214 * @throws DatabaseException
215 * @throws Exception
854f78a Last lot of code formatting to PSR-2
John Porter authored
216 * @param integer|array $id
217 * The ID of the field to retrieve. Defaults to null which will return multiple field
218 * objects. Since Symphony 2.3, `$id` will accept an array of Field ID's
219 * @param integer $section_id
220 * The ID of the section to look for the fields in. Defaults to null which will allow
221 * all fields in the Symphony installation to be searched on.
222 * @param string $order
223 * Available values of ASC (Ascending) or DESC (Descending), which refer to the
224 * sort order for the query. Defaults to ASC (Ascending)
225 * @param string $sortfield
226 * The field to sort the query by. Can be any from the tbl_fields schema. Defaults to
227 * 'sortorder'
228 * @param string $type
229 * Filter fields by their type, ie. input, select. Defaults to null
230 * @param string $location
231 * Filter fields by their location in the entry form. There are two possible values,
232 * 'main' or 'sidebar'. Defaults to null
233 * @param string $where
234 * Allows a custom where query to be included. Must be valid SQL. The tbl_fields alias
235 * is t1
236 * @param integer|string $restrict
237 * Only return fields if they match one of the Field Constants. Available values are
238 * `__TOGGLEABLE_ONLY__`, `__UNTOGGLEABLE_ONLY__`, `__FILTERABLE_ONLY__`,
239 * `__UNFILTERABLE_ONLY__` or `__FIELD_ALL__`. Defaults to `__FIELD_ALL__`
240 * @return array
241 * An array of Field objects. If no Field are found, null is returned.
242 */
243 public static function fetch($id = null, $section_id = null, $order = 'ASC', $sortfield = 'sortorder', $type = null, $location = null, $where = null, $restrict = Field::__FIELD_ALL__)
244 {
245 $fields = array();
246 $returnSingle = false;
247 $ids = array();
248 $field_contexts = array();
249
250 if (!is_null($id)) {
251 if (is_numeric($id)) {
252 $returnSingle = true;
253 }
254
255 if (!is_array($id)) {
256 $field_ids = array((int)$id);
257 } else {
258 $field_ids = $id;
259 }
260
261 // Loop over the `$field_ids` and check to see we have
262 // instances of the request fields
263 foreach ($field_ids as $key => $field_id) {
264 if (
265 isset(self::$_initialiased_fields[$field_id])
266 && self::$_initialiased_fields[$field_id] instanceof Field
267 ) {
268 $fields[$field_id] = self::$_initialiased_fields[$field_id];
269 unset($field_ids[$key]);
270 }
271 }
272 }
273
274 // If there is any `$field_ids` left to be resolved lets do that, otherwise
275 // if `$id` wasn't provided in the first place, we'll also continue
276 if (!empty($field_ids) || is_null($id)) {
277 $sql = sprintf(
278 "SELECT t1.*
279 FROM tbl_fields AS `t1`
280 WHERE 1
281 %s %s %s %s
282 %s",
283 (isset($type) ? " AND t1.`type` = '{$type}' " : null),
284 (isset($location) ? " AND t1.`location` = '{$location}' " : null),
285 (isset($section_id) ? " AND t1.`parent_section` = '{$section_id}' " : null),
286 $where,
287 isset($field_ids) ? " AND t1.`id` IN(" . implode(',', $field_ids) . ") " : " ORDER BY t1.`{$sortfield}` {$order}"
288 );
289
290 if (!$result = Symphony::Database()->fetch($sql)) {
291 return ($returnSingle ? null : array());
292 }
293
294 // Loop over the resultset building an array of type, field_id
295 foreach ($result as $f) {
296 $ids[$f['type']][] = $f['id'];
297 }
298
299 // Loop over the `ids` array, which is grouped by field type
300 // and get the field context.
301 foreach ($ids as $type => $field_id) {
302 $field_contexts[$type] = Symphony::Database()->fetch(sprintf(
303 "SELECT * FROM `tbl_fields_%s` WHERE `field_id` IN (%s)",
304 $type,
305 implode(',', $field_id)
306 ), 'field_id');
307 }
308
309 foreach ($result as $f) {
310 // We already have this field in our static store
311 if (
312 isset(self::$_initialiased_fields[$f['id']])
313 && self::$_initialiased_fields[$f['id']] instanceof Field
314 ) {
315 $field = self::$_initialiased_fields[$f['id']];
316
317 // We don't have an instance of this field, so let's set one up
318 } else {
319 $field = self::create($f['type']);
320 $field->setArray($f);
8cb0861 @brendo Now that the show_association value is not stored on field's, the Fie…
brendo authored
321 // If the field has said that's going to have associations, then go find the
322 // association setting value. In future this check will be most robust with
323 // an interface, but for now, this is what we've got. RE: #2082
324 if ($field->canShowAssociationColumn()) {
325 $field->set('show_association', SectionManager::getSectionAssociationSetting($f['id']));
326 }
854f78a Last lot of code formatting to PSR-2
John Porter authored
327
328 // Get the context for this field from our previous queries.
329 $context = $field_contexts[$f['type']][$f['id']];
330
331 if (is_array($context) && !empty($context)) {
332 try {
333 unset($context['id']);
334 $field->setArray($context);
335 } catch (Exception $e) {
336 throw new Exception(__(
337 'Settings for field %s could not be found in table tbl_fields_%s.',
338 array($f['id'], $f['type'])
339 ));
340 }
341 }
342
343 self::$_initialiased_fields[$f['id']] = $field;
344 }
345
346 // Check to see if there was any restricts imposed on the fields
347 if (
348 $restrict == Field::__FIELD_ALL__
349 || ($restrict == Field::__TOGGLEABLE_ONLY__ && $field->canToggle())
350 || ($restrict == Field::__UNTOGGLEABLE_ONLY__ && !$field->canToggle())
351 || ($restrict == Field::__FILTERABLE_ONLY__ && $field->canFilter())
352 || ($restrict == Field::__UNFILTERABLE_ONLY__ && !$field->canFilter())
353 ) {
354 $fields[$f['id']] = $field;
355 }
356 }
357 }
358
359 return count($fields) <= 1 && $returnSingle ? current($fields) : $fields;
360 }
361
362 /**
363 * Given a field ID, return the type of the field by querying `tbl_fields`
364 *
365 * @param integer $id
366 * @return string
367 */
368 public static function fetchFieldTypeFromID($id)
369 {
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
370 return Symphony::Database()->fetchVar('type', 0, sprintf("
371 SELECT `type` FROM `tbl_fields` WHERE `id` = %d LIMIT 1",
372 $id
373 ));
854f78a Last lot of code formatting to PSR-2
John Porter authored
374 }
375
376 /**
377 * Given a field ID, return the handle of the field by querying `tbl_fields`
378 *
379 * @param integer $id
380 * @return string
381 */
382 public static function fetchHandleFromID($id)
383 {
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
384 return Symphony::Database()->fetchVar('element_name', 0, sprintf("
385 SELECT `element_name` FROM `tbl_fields` WHERE `id` = %d LIMIT 1",
386 $id
387 ));
854f78a Last lot of code formatting to PSR-2
John Porter authored
388 }
389
390 /**
391 * Given an `$element_name` and a `$section_id`, return the Field ID. Symphony enforces
392 * a uniqueness constraint on a section where every field must have a unique
393 * label (and therefore handle) so whilst it is impossible to have two fields
394 * from the same section, it would be possible to have two fields with the same
395 * name from different sections. Passing the `$section_id` lets you to specify
396 * which section should be searched. If `$element_name` is null, this function will
397 * return all the Field ID's from the given `$section_id`.
398 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
399 * @throws DatabaseException
854f78a Last lot of code formatting to PSR-2
John Porter authored
400 * @since Symphony 2.3 This function can now accept $element_name as an array
401 * of handles. These handles can now also include the handle's mode, eg. `title: formatted`
402 * @param string|array $element_name
403 * The handle of the Field label, or an array of handles. These handles may contain
404 * a mode as well, eg. `title: formatted`.
405 * @param integer $section_id
406 * The section that this field belongs too
407 * The field ID, or an array of field ID's
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
408 * @return mixed
854f78a Last lot of code formatting to PSR-2
John Porter authored
409 */
410 public static function fetchFieldIDFromElementName($element_name, $section_id = null)
411 {
412 if (is_null($element_name)) {
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
413 $schema_sql = sprintf("
414 SELECT `id`
854f78a Last lot of code formatting to PSR-2
John Porter authored
415 FROM `tbl_fields`
416 WHERE `parent_section` = %d
417 ORDER BY `sortorder` ASC",
418 $section_id
419 );
420 } else {
421 $element_names = !is_array($element_name) ? array($element_name) : $element_name;
422
423 // allow for pseudo-fields containing colons (e.g. Textarea formatted/unformatted)
424 foreach ($element_names as $index => $name) {
425 $parts = explode(':', $name, 2);
426
427 if (count($parts) == 1) {
428 continue;
429 }
430
431 unset($element_names[$index]);
432
433 // Prevent attempting to look up 'system', which will arise
434 // from `system:pagination`, `system:id` etc.
435 if ($parts[0] == 'system') {
436 continue;
437 }
438
439 $element_names[] = Symphony::Database()->cleanValue(trim($parts[0]));
440 }
441
7b74a6e @brendo Be stricter when constructing queries throughout the core. RE: #2190
brendo authored
442 $schema_sql = empty($element_names) ? null : sprintf("
443 SELECT `id`
854f78a Last lot of code formatting to PSR-2
John Porter authored
444 FROM `tbl_fields`
445 WHERE 1
446 %s
447 AND `element_name` IN ('%s')
448 ORDER BY `sortorder` ASC",
449 (!is_null($section_id) ? sprintf("AND `parent_section` = %d", $section_id) : ""),
450 implode("', '", array_unique($element_names))
451 );
452 }
453
454 if (is_null($schema_sql)) {
455 return false;
456 }
457
458 $result = Symphony::Database()->fetch($schema_sql);
459
460 if (count($result) == 1) {
461 return (int)$result[0]['id'];
462 } elseif (empty($result)) {
463 return false;
464 } else {
465 foreach ($result as &$r) {
466 $r = (int)$r['id'];
467 }
468
469 return $result;
470 }
471 }
472
473 /**
474 * Work out the next available sort order for a new field
475 *
476 * @return integer
477 * Returns the next sort order
478 */
479 public static function fetchNextSortOrder()
480 {
481 $next = Symphony::Database()->fetchVar(
482 "next",
483 0,
484 "SELECT
485 MAX(p.sortorder) + 1 AS `next`
486 FROM
487 `tbl_fields` AS p
488 LIMIT 1"
489 );
490 return ($next ? (int)$next : 1);
491 }
492
493 /**
494 * Given a `$section_id`, this function returns an array of the installed
495 * fields schema. This includes the `id`, `element_name`, `type`
496 * and `location`.
497 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
498 * @throws DatabaseException
854f78a Last lot of code formatting to PSR-2
John Porter authored
499 * @since Symphony 2.3
500 * @param integer $section_id
501 * @return array
502 * An associative array that contains four keys, `id`, `element_name`,
503 * `type` and `location`
504 */
505 public static function fetchFieldsSchema($section_id)
506 {
507 return Symphony::Database()->fetch(sprintf(
508 "SELECT `id`, `element_name`, `type`, `location`
509 FROM `tbl_fields`
510 WHERE `parent_section` = %d
511 ORDER BY `sortorder` ASC",
512 $section_id
513 ));
514 }
515
516 /**
517 * Returns an array of all available field handles discovered in the
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
518 * `TOOLKIT . /fields` or `EXTENSIONS . /extension_handle/fields`.
854f78a Last lot of code formatting to PSR-2
John Porter authored
519 *
520 * @return array
521 * A single dimensional array of field handles.
522 */
523 public static function listAll()
524 {
525 $structure = General::listStructure(TOOLKIT . '/fields', '/field.[a-z0-9_-]+.php/i', false, 'asc', TOOLKIT . '/fields');
526 $extensions = Symphony::ExtensionManager()->listInstalledHandles();
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
527 $types = array();
854f78a Last lot of code formatting to PSR-2
John Porter authored
528
529 if (is_array($extensions) && !empty($extensions)) {
530 foreach ($extensions as $handle) {
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
531 $path = EXTENSIONS . '/' . $handle . '/fields';
532 if (is_dir($path)) {
533 $tmp = General::listStructure($path, '/field.[a-z0-9_-]+.php/i', false, 'asc', $path);
854f78a Last lot of code formatting to PSR-2
John Porter authored
534
535 if (is_array($tmp['filelist']) && !empty($tmp['filelist'])) {
536 $structure['filelist'] = array_merge($structure['filelist'], $tmp['filelist']);
537 }
538 }
539 }
540
541 $structure['filelist'] = General::array_remove_duplicates($structure['filelist']);
542 }
543
544 foreach ($structure['filelist'] as $filename) {
545 $types[] = self::__getHandleFromFilename($filename);
546 }
547
548 return $types;
549 }
550
551 /**
552 * Creates an instance of a given class and returns it. Adds the instance
553 * to the `$_pool` array with the key being the handle.
554 *
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
555 * @throws Exception
854f78a Last lot of code formatting to PSR-2
John Porter authored
556 * @param string $type
557 * The handle of the Field to create (which is it's handle)
558 * @return Field
559 */
560 public static function create($type)
561 {
562 if (!isset(self::$_pool[$type])) {
563 $classname = self::__getClassName($type);
564 $path = self::__getDriverPath($type);
565
566 if (!file_exists($path)) {
567 throw new Exception(
568 __('Could not find Field %1$s at %2$s.', array('<code>' . $type . '</code>', '<code>' . $path . '</code>'))
569 . ' ' . __('If it was provided by an Extension, ensure that it is installed, and enabled.')
570 );
571 }
572
573 if (!class_exists($classname)) {
574 require_once($path);
575 }
576
577 self::$_pool[$type] = new $classname;
578
579 if (self::$_pool[$type]->canShowTableColumn() && !self::$_pool[$type]->get('show_column')) {
580 self::$_pool[$type]->set('show_column', 'yes');
581 }
582 }
583
584 return clone self::$_pool[$type];
585 }
586
587 /**
588 * Return boolean if the given `$field_type` is in use anywhere in the
589 * current Symphony install.
590 *
591 * @since Symphony 2.3
592 * @param string $field_type
593 * @return boolean
594 */
595 public static function isFieldUsed($field_type)
596 {
597 return Symphony::Database()->fetchVar('count', 0, sprintf(
598 "SELECT COUNT(*) AS `count` FROM `tbl_fields` WHERE `type` = '%s'",
599 $field_type
600 )) > 0;
601 }
602
603 /**
604 * Check if a specific text formatter is used by a Field
605 *
606 * @since Symphony 2.3
f8a1473 @brendo Minor PHPDoc fixes
brendo authored
607 * @param string $text_formatter_handle
854f78a Last lot of code formatting to PSR-2
John Porter authored
608 * The handle of the `TextFormatter`
609 * @return boolean
610 * true if used, false if not
611 */
612 public static function isTextFormatterUsed($text_formatter_handle)
613 {
614 $fields = Symphony::Database()->fetchCol('type', "SELECT DISTINCT `type` FROM `tbl_fields` WHERE `type` NOT IN ('author', 'checkbox', 'date', 'input', 'select', 'taglist', 'upload')");
615
616 if (!empty($fields)) {
617 foreach ($fields as $field) {
618 try {
619 $table = Symphony::Database()->fetchVar('count', 0, sprintf(
620 "SELECT COUNT(*) AS `count`
621 FROM `tbl_fields_%s`
622 WHERE `formatter` = '%s'",
623 Symphony::Database()->cleanValue($field),
624 $text_formatter_handle
625 ));
626 } catch (DatabaseException $ex) {
627 // Table probably didn't have that column
628 }
629
630 if ($table > 0) {
631 return true;
632 }
633 }
634 }
635
636 return false;
637 }
638 }
Something went wrong with that request. Please try again.