symphonycms/symphony-2

View on GitHub
symphony/lib/toolkit/class.fieldmanager.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * @package toolkit
 */
/**
 * The `FieldManager` class is responsible for managing all fields types in Symphony.
 * Fields are stored on the file system either in the `/fields` folder of `TOOLKIT` or
 * in a `fields` folder in an extension directory.
 */

class FieldManager implements FileResource
{
    /**
     * An array of all the objects that the Manager is responsible for.
     * Defaults to an empty array.
     * @var array
     */
    protected static $_pool = array();

    /**
     * An array of all fields whose have been created by ID
     * @var array
     */
    private static $_initialiased_fields = array();

    /**
     * Given the filename of a Field, return it's handle. This will remove
     * the Symphony conventions of `field.*.php`
     *
     * @param string $filename
     *  The filename of the Field
     * @return string
     */
    public static function __getHandleFromFilename($filename)
    {
        return preg_replace(array('/^field./i', '/.php$/i'), '', $filename);
    }

    /**
     * Given a type, returns the full class name of a Field. Fields use a
     * 'field' prefix
     *
     * @param string $type
     *  A field handle
     * @return string
     */
    public static function __getClassName($type)
    {
        return 'field' . $type;
    }

    /**
     * Finds a Field by type by searching the `TOOLKIT . /fields` folder and then
     * any fields folders in the installed extensions. The function returns
     * the path to the folder where the field class resides.
     *
     * @param string $type
     *  The field handle, that is, `field.{$handle}.php`
     * @return string|boolean
     */
    public static function __getClassPath($type)
    {
        if (is_file(TOOLKIT . "/fields/field.{$type}.php")) {
            return TOOLKIT . '/fields';
        } else {
            $extensions = Symphony::ExtensionManager()->listInstalledHandles();

            if (is_array($extensions) && !empty($extensions)) {
                foreach ($extensions as $e) {
                    if (is_file(EXTENSIONS . "/{$e}/fields/field.{$type}.php")) {
                        return EXTENSIONS . "/{$e}/fields";
                    }
                }
            }
        }

        return false;
    }

    /**
     * Given a field type, return the path to it's class
     *
     * @see __getClassPath()
     * @param string $type
     *  The handle of the field to load (it's type)
     * @return string
     */
    public static function __getDriverPath($type)
    {
        return self::__getClassPath($type) . "/field.{$type}.php";
    }

    /**
     * This function is not implemented by the `FieldManager` class
     *
     * @return boolean
     */
    public static function about($name)
    {
        return false;
    }

    /**
     * Given an associative array of fields, insert them into the database
     * returning the resulting Field ID if successful, or false if there
     * was an error. As fields are saved in order on a section, a query is
     * made to determine the sort order of this field to be current sort order
     * +1.
     *
     * @throws DatabaseException
     * @param array $fields
     *  Associative array of field names => values for the Field object
     * @return integer
     *  Returns a Field ID of the created Field on success, 0 otherwise.
     */
    public static function add(array $fields)
    {
        if (!isset($fields['sortorder'])) {
            $fields['sortorder'] = self::fetchNextSortOrder();
        }

        $inserted = Symphony::Database()
            ->insert('tbl_fields')
            ->values($fields)
            ->execute()
            ->success();

        return $inserted ? Symphony::Database()->getInsertID() : 0;
    }

    /**
     * Save the settings for a Field given it's `$field_id` and an associative
     * array of settings.
     *
     * @throws DatabaseException
     * @since Symphony 2.3
     * @param integer $field_id
     *  The ID of the field
     * @param array $settings
     *  An associative array of settings, where the key is the column name
     *  and the value is the value.
     * @return boolean
     *  true on success, false on failure
     */
    public static function saveSettings($field_id, $settings)
    {
        return Symphony::Database()->transaction(function (Database $db) use ($field_id, $settings) {
            // Get the type of this field:
            $type = self::fetchFieldTypeFromID($field_id);

            if (!$type) {
                throw new Exception("Field id `$field_id` does not map to a field");
            }

            // Delete the original settings:
            $db
                ->delete("tbl_fields_$type")
                ->where(['field_id' => $field_id])
                ->limit(1)
                ->execute();

            // Insert the new settings into the type table:
            if (!isset($settings['field_id'])) {
                $settings['field_id'] = $field_id;
            }

            $db
                ->insert("tbl_fields_$type")
                ->values($settings)
                ->execute();
        })->execute()->success();
    }

    /**
     * Given a Field ID and associative array of fields, update an existing Field
     * row in the `tbl_fields`table. Returns boolean for success/failure
     *
     * @throws DatabaseException
     * @param integer $id
     *  The ID of the Field that should be updated
     * @param array $fields
     *  Associative array of field names => values for the Field object
     *  This array does need to contain every value for the field object, it
     *  can just be the changed values.
     * @return boolean
     */
    public static function edit($id, array $fields)
    {
        return Symphony::Database()
            ->update('tbl_fields')
            ->set($fields)
            ->where(['id' => (int)$id])
            ->execute()
            ->success();
    }

    /**
     * Given a Field ID, delete a Field from Symphony. This will remove the field from
     * the fields table, all of the data stored in this field's `tbl_entries_data_$id` any
     * existing section associations. This function additionally call the Field's `tearDown`
     * method so that it can cleanup any additional settings or entry tables it may of created.
     *
     * @since Symphony 2.7.0 it will check to see if the field requires a data table before
     * blindly trying to delete it.
     *
     * @throws DatabaseException
     * @throws Exception
     * @param integer $id
     *  The ID of the Field that should be deleted
     * @return boolean
     */
    public static function delete($id)
    {
        $existing = (new FieldManager)->select()->field($id)->execute()->next();
        if (!$existing) {
            return true;
        }
        $existing->tearDown();

        Symphony::Database()
            ->delete('tbl_fields')
            ->where(['id' => (int)$id])
            ->execute();
        Symphony::Database()
            ->delete('tbl_fields_' . $existing->handle())
            ->where(['field_id' => (int)$id])
            ->execute();
        SectionManager::removeSectionAssociation($id);

        if ($existing->requiresTable()) {
            return Symphony::Database()
                ->drop("tbl_entries_data_$id")
                ->ifExists()
                ->execute()
                ->success();
        }

        return true;
    }

    /**
     * @internal Checks if we already have a Field object for this $field_id.
     *
     * @since Symphony 3.0.0
     * @param int $field_id
     *  The field id to look for
     * @return Field
     *  The Field object instance, if it exists. null otherwise.
     */
    public static function getInitializedField($field_id)
    {
        if (isset(self::$_initialiased_fields[$field_id]) &&
            self::$_initialiased_fields[$field_id] instanceof Field) {
            return self::$_initialiased_fields[$field_id];
        }
        return null;
    }

    /**
     * @internal Sets a Field object in the static store.
     *
     * @since Symphony 3.0.0
     * @throws Exception
     *  If the Field is already in the cache, an Exception is thrown.
     * @param Field $field
     *  The Field object to store
     * @return void
     */
    public static function setInitializedField(Field $field)
    {
        $field_id = $field->get('id');
        if (self::getInitializedField($field_id)) {
            throw new Exception('Field is already in the cache');
        }
        self::$_initialiased_fields[$field_id] = $field;
    }

    /**
     * The fetch method returns a instance of a Field from tbl_fields. The most common
     * use of this function is to retrieve a Field by ID, but it can be used to retrieve
     * Fields from a Section also. There are several parameters that can be used to fetch
     * fields by their Type, Location, by a Field Constant or with a custom WHERE query.
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select() instead
     * @throws DatabaseException
     * @throws Exception
     * @param integer|array $id
     *  The ID of the field to retrieve. Defaults to null which will return multiple field
     *  objects. Since Symphony 2.3, `$id` will accept an array of Field ID's
     * @param integer $section_id
     *  The ID of the section to look for the fields in. Defaults to null which will allow
     *  all fields in the Symphony installation to be searched on.
     * @param string $order
     *  Available values of ASC (Ascending) or DESC (Descending), which refer to the
     *  sort order for the query. Defaults to ASC (Ascending)
     * @param string $sortfield
     *  The field to sort the query by. Can be any from the tbl_fields schema. Defaults to
     *  'sortorder'
     * @param string $type
     *  Filter fields by their type, ie. input, select. Defaults to null
     * @param string $location
     *  Filter fields by their location in the entry form. There are two possible values,
     *  'main' or 'sidebar'. Defaults to null
     * @param string $where
     *  Allows a custom where query to be included. Must be valid SQL. The tbl_fields alias
     *  is t1
     * @param integer|string $restrict
     *  Only return fields if they match one of the Field Constants. Available values are
     *  `__TOGGLEABLE_ONLY__`, `__UNTOGGLEABLE_ONLY__`, `__FILTERABLE_ONLY__`,
     *  `__UNFILTERABLE_ONLY__` or `__FIELD_ALL__`. Defaults to `__FIELD_ALL__`
     * @return array
     *  An array of Field objects. If no Field are found, null is returned.
     */
    public static function fetch($id = null, $section_id = null, $order = 'ASC', $sortfield = 'sortorder', $type = null, $location = null, $where = null, $restrict = Field::__FIELD_ALL__)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog('FieldManager::fetch()', 'FieldManager::select()');
        }

        $fields = [];
        $returnSingle = false;
        $ids = [];
        $field_contexts = [];

        if (!is_null($id)) {
            if (is_numeric($id)) {
                $returnSingle = true;
            }

            if (!is_array($id)) {
                $field_ids = array((int)$id);
            } else {
                $field_ids = $id;
            }

            // Loop over the `$field_ids` and check to see we have
            // instances of the request fields
            foreach ($field_ids as $key => $field_id) {
                if ($if = self::getInitializedField($field_id)) {
                    $fields[$field_id] = $if;
                    unset($field_ids[$key]);
                }
            }
        }

        // If there is any `$field_ids` left to be resolved lets do that, otherwise
        // if `$id` wasn't provided in the first place, we'll also continue
        if (!empty($field_ids) || is_null($id)) {
            $query = (new FieldManager)->select();

            if ($type) {
                $query->type($type);
            }
            if ($location) {
                $query->location($location);
            }
            if ($section_id) {
                $query->section($section_id);
            }
            if ($field_ids) {
                $query->fields($field_ids);
            }
            if ($where) {
                $where = $query->replaceTablePrefix($where);
                // Replace legacy `t1` alias
                $where = str_replace('t1.', '`f`.', $where);
                $where = str_replace('`t1`.', '`f`.', $where);
                // Ugly hack: mysqli allowed this....
                $where = str_replace('IN ()', 'IN (0)', $where);
                $wherePrefix = $query->containsSQLParts('where') ? '' : 'WHERE 1 = 1';
                $query->unsafe()->unsafeAppendSQLPart('where', "$wherePrefix $where");
            }
            if ($sortfield) {
                $query->sort((string)$sortfield);
            }

            $result = $query->execute()->rows();

            if (empty($result)) {
                return ($returnSingle ? null : []);
            }
            foreach ($result as $field) {
                $fields[$field->get('id')] = $field;
            }
        }

        return count($fields) <= 1 && $returnSingle ? current($fields) : $fields;
    }

    /**
     * Given a field ID, return the type of the field by querying `tbl_fields`
     *
     * @param integer $id
     * @return string
     */
    public static function fetchFieldTypeFromID($id)
    {
        return Symphony::Database()
            ->select(['type'])
            ->from('tbl_fields')
            ->where(['id' => (int)$id])
            ->limit(1)
            ->execute()
            ->string('type');
    }

    /**
     * Given a field ID, return the handle of the field by querying `tbl_fields`
     *
     * @param integer $id
     * @return string
     */
    public static function fetchHandleFromID($id)
    {
        return Symphony::Database()
            ->select(['element_name'])
            ->from('tbl_fields')
            ->where(['id' => (int)$id])
            ->limit(1)
            ->execute()
            ->string('element_name');
    }

    /**
     * Given an `$element_name` and a `$section_id`, return the Field ID. Symphony enforces
     * a uniqueness constraint on a section where every field must have a unique
     * label (and therefore handle) so whilst it is impossible to have two fields
     * from the same section, it would be possible to have two fields with the same
     * name from different sections. Passing the `$section_id` lets you to specify
     * which section should be searched. If `$element_name` is null, this function will
     * return all the Field ID's from the given `$section_id`.
     *
     * @throws DatabaseException
     * @since Symphony 2.3 This function can now accept $element_name as an array
     *  of handles. These handles can now also include the handle's mode, eg. `title: formatted`
     * @param string|array $element_name
     *  The handle of the Field label, or an array of handles. These handles may contain
     *  a mode as well, eg. `title: formatted`.
     * @param integer $section_id
     *  The section that this field belongs too
     *  The field ID, or an array of field ID's
     * @return mixed
     */
    public static function fetchFieldIDFromElementName($element_name, $section_id = null)
    {
        $schema_sql = Symphony::Database()
            ->select(['id'])
            ->from('tbl_fields')
            ->orderBy(['sortorder' => 'ASC'])
            ->usePlaceholders();

        if ($element_name) {
            $element_names = !is_array($element_name) ? [$element_name] : $element_name;

            // allow for pseudo-fields containing colons (e.g. Textarea formatted/unformatted)
            foreach ($element_names as $index => $name) {
                $parts = explode(':', $name, 2);

                if (count($parts) == 1) {
                    continue;
                }

                unset($element_names[$index]);

                // Prevent attempting to look up 'system', which will arise
                // from `system:pagination`, `system:id` etc.
                if ($parts[0] == 'system') {
                    continue;
                }

                $element_names[] = trim($parts[0]);
            }

            if (!empty($element_names)) {
                $schema_sql->where(['element_name' => ['in' => array_unique($element_names)]]);
            }
        }

        if ($section_id) {
            $schema_sql->where(['parent_section' => $section_id]);
        }

        $result = $schema_sql->execute()->column('id');

        if (empty($result)) {
            return false;
        } elseif (count($result) === 1) {
            return (int)$result[0];
        }
        return array_map('intval', $result);
    }

    /**
     * Work out the next available sort order for a new field
     *
     * @return integer
     *  Returns the next sort order
     */
    public static function fetchNextSortOrder()
    {
        $next = Symphony::Database()
            ->select(['MAX(sortorder)'])
            ->from('tbl_fields')
            ->execute()
            ->integer(0);

        return $next + 1;
    }

    /**
     * Given a `$section_id`, this function returns an array of the installed
     * fields schema. This includes the `id`, `element_name`, `type`
     * and `location`.
     *
     * @throws DatabaseException
     * @since Symphony 2.3
     * @param integer $section_id
     * @return array
     *  An array of associative arrays that contains four keys, `id`, `element_name`,
     * `type` and `location`
     */
    public static function fetchFieldsSchema($section_id)
    {
        return Symphony::Database()
            ->select(['id', 'element_name', 'type', 'location'])
            ->from('tbl_fields')
            ->where(['parent_section' => $section_id])
            ->orderBy(['sortorder' => 'ASC'])
            ->execute()
            ->rows();
    }

    /**
     * Returns an array of all available field handles discovered in the
     * `TOOLKIT . /fields` or `EXTENSIONS . /extension_handle/fields`.
     *
     * @return array
     *  A single dimensional array of field handles.
     */
    public static function listAll()
    {
        $structure = General::listStructure(TOOLKIT . '/fields', '/field.[a-z0-9_-]+.php/i', false, 'asc', TOOLKIT . '/fields');
        $extensions = Symphony::ExtensionManager()->listInstalledHandles();
        $types = array();

        if (is_array($extensions) && !empty($extensions)) {
            foreach ($extensions as $handle) {
                $path = EXTENSIONS . '/' . $handle . '/fields';
                if (is_dir($path)) {
                    $tmp = General::listStructure($path, '/field.[a-z0-9_-]+.php/i', false, 'asc', $path);

                    if (is_array($tmp['filelist']) && !empty($tmp['filelist'])) {
                        $structure['filelist'] = array_merge($structure['filelist'], $tmp['filelist']);
                    }
                }
            }

            $structure['filelist'] = General::array_remove_duplicates($structure['filelist']);
        }

        foreach ($structure['filelist'] as $filename) {
            $types[] = self::__getHandleFromFilename($filename);
        }

        return $types;
    }

    /**
     * Creates an instance of a given class and returns it. Adds the instance
     * to the `$_pool` array with the key being the handle.
     *
     * @throws Exception
     * @param string $type
     *  The handle of the Field to create (which is it's handle)
     * @return Field
     */
    public static function create($type)
    {
        if (!isset(self::$_pool[$type])) {
            $classname = self::__getClassName($type);
            $path = self::__getDriverPath($type);

            if (!file_exists($path)) {
                throw new Exception(
                    __('Could not find Field %1$s at %2$s.', array('<code>' . $type . '</code>', '<code>' . $path . '</code>'))
                    . ' ' . __('If it was provided by an Extension, ensure that it is installed, and enabled.')
                );
            }

            if (!class_exists($classname)) {
                require_once($path);
            }

            self::$_pool[$type] = new $classname;

            if (self::$_pool[$type]->canShowTableColumn() && !self::$_pool[$type]->get('show_column')) {
                self::$_pool[$type]->set('show_column', 'yes');
            }
        }

        return clone self::$_pool[$type];
    }

    /**
     * Return boolean if the given `$field_type` is in use anywhere in the
     * current Symphony install.
     *
     * @since Symphony 2.3
     * @param string $field_type
     * @return boolean
     */
    public static function isFieldUsed($field_type)
    {
        return Symphony::Database()
            ->select()
            ->count()
            ->from('tbl_fields')
            ->where(['type' => $field_type])
            ->execute()
            ->integer(0) > 0;
    }

    /**
     * Check if a specific text formatter is used by a Field
     *
     * @since Symphony 2.3
     * @param string $text_formatter_handle
     *  The handle of the `TextFormatter`
     * @return boolean
     *  true if used, false if not
     */
    public static function isTextFormatterUsed($text_formatter_handle)
    {
        $fields = Symphony::Database()
            ->select(['type'])
            ->distinct()
            ->from('tbl_fields')
            ->where(['type' => ['not in' => [
                'author', 'checkbox', 'date', 'input', 'select', 'taglist', 'upload'
            ]]])
            ->execute()
            ->column('type');

        if (!empty($fields)) {
            foreach ($fields as $field) {
                $table = 0;
                try {
                    $table = Symphony::Database()
                        ->select()
                        ->count()
                        ->from("tbl_fields_$field")
                        ->where(['formatter' => $text_formatter_handle])
                        ->execute()
                        ->integer(0);
                } catch (DatabaseException $ex) {
                    // Table probably didn't have that column
                }

                if ($table > 0) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Factory method that creates a new FieldQuery.
     *
     * @since Symphony 3.0.0
     * @param array $projection
     *  The projection to select.
     *  If no projection gets added, it defaults to `FieldQuery::getDefaultProjection()`.
     * @return FieldQuery
     */
    public function select(array $projection = [])
    {
        return new FieldQuery(Symphony::Database(), $projection);
    }
}