symphonycms/symphony-2

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

Summary

Maintainability
F
3 days
Test Coverage
<?php

/**
 * @package toolkit
 */
/**
 * The `EntryManager` is responsible for all `Entry` objects in Symphony.
 * Entries are stored in the database in a cluster of tables. There is a
 * parent entry row stored in `tbl_entries` and then each field's data is
 * stored in a separate table, `tbl_entries_data_{field_id}`. Where Field ID
 * is generated when the Section is saved. This Manager provides basic
 * add, edit, delete and fetching methods for Entries.
 */
class EntryManager
{
    /**
     * The Field ID that will be used to sort when fetching Entries, defaults
     * to null, which implies the Entry ID (id column in `tbl_entries`).
     * To order by core fields, use one of
     * 'system:creation-date', 'system:modification-date', 'system:id'.
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @var integer|string
     */
    protected static $_fetchSortField = null;

    /**
     * The direction that entries should be sorted in, available options are
     * RAND, ASC or DESC. Defaults to null, which implies ASC
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @var string
     */
    protected static $_fetchSortDirection = null;

    /**
     * Setter function for the default sorting direction of the Fetch
     * function. Available options are RAND, ASC or DESC.
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @param string $direction
     *  The direction that entries should be sorted in, available options
     *  are RAND, ASC or DESC.
     */
    public static function setFetchSortingDirection($direction)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog(
                'EntryManager::setFetchSortingDirection()',
                'EntryManager::select()->sort()'
            );
        }

        $direction = strtoupper($direction);

        if ($direction == 'RANDOM') {
            $direction = 'RAND';
        }

        self::$_fetchSortDirection = (in_array($direction, array('RAND', 'ASC', 'DESC')) ? $direction : null);
    }

    /**
     * Sets the field to applying the sorting direction on when fetching
     * entries
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @param integer $field_id
     *  The ID of the Field that should be sorted on
     */
    public static function setFetchSortingField($field_id)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog(
                'EntryManager::setFetchSortingField()',
                'EntryManager::select()->sort()'
            );
        }
        self::$_fetchSortField = $field_id;
    }

    /**
     * Convenience function that will set sorting field and direction
     * by calling `setFetchSortingField()` & `setFetchSortingDirection()`
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @see toolkit.EntryManager#setFetchSortingField()
     * @see toolkit.EntryManager#setFetchSortingDirection()
     * @param integer $field_id
     *  The ID of the Field that should be sorted on
     * @param string $direction
     *  The direction that entries should be sorted in, available options
     *  are RAND, ASC or DESC. Defaults to ASC
     */
    public static function setFetchSorting($field_id, $direction = 'ASC')
    {
        self::setFetchSortingField($field_id);
        self::setFetchSortingDirection($direction);
    }

    /**
     * Returns an object representation of the sorting for the
     * EntryManager, with the field and direction provided
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select()->sort() instead
     * @return StdClass
     */
    public static function getFetchSorting()
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog(
                'EntryManager::getFetchSorting()',
                'EntryManager::select()->sort()'
            );
        }
        return (object)array(
            'field' => self::$_fetchSortField,
            'direction' => self::$_fetchSortDirection
        );
    }

    /**
     * Executes the SQL queries need to save a field's data for the specified
     * entry id.
     *
     * It first locks the table for writes, it then deletes existing data and then
     * it inserts a new row for the data. Errors are discarded and the lock is
     * released, if it was acquired.
     *
     * @param int $entry_id
     *  The entry id to save the data for
     * @param int $field_id
     *  The field id to save the data for
     * @param array $field
     *  The field data to save
     */
    protected static function saveFieldData($entry_id, $field_id, $field)
    {
        // Check that we have a field id
        if (empty($field_id)) {
            return;
        }

        // Ignore parameter when not an array
        if (!is_array($field)) {
            $field = [];
        }

        // Check if table exists
        $table_name = 'tbl_entries_data_' . General::intval($field_id);
        if (!Symphony::Database()->tableExists($table_name)) {
            return;
        }

        // Delete old data
        Symphony::Database()
            ->delete($table_name)
            ->where(['entry_id' => $entry_id])
            ->execute();

        // Insert new data
        $data = [
            'entry_id' => $entry_id
        ];

        $fields = [];

        foreach ($field as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $ii => $v) {
                    $fields[$ii][$key] = $v;
                }
            } else {
                $fields[max(0, count($fields) - 1)][$key] = $value;
            }
        }

        foreach ($fields as $index => $field_data) {
            $fields[$index] = array_merge($data, $field_data);
        }

        // Insert only if we have field data
        if (!empty($fields)) {
            foreach ($fields as $f) {
                Symphony::Database()
                    ->insert($table_name)
                    ->values($f)
                    ->execute();
            }
        }
    }

    /**
     * Given an Entry object, iterate over all of the fields in that object
     * an insert them into their relevant entry tables.
     *
     * @see EntryManager::saveFieldData()
     * @param Entry $entry
     *  An Entry object to insert into the database
     * @throws DatabaseException
     * @return boolean
     */
    public static function add(Entry $entry)
    {
        return Symphony::Database()->transaction(function (Database $db) use ($entry) {
            $fields = $entry->get();
            $inserted = $db
                ->insert('tbl_entries')
                ->values($fields)
                ->execute()
                ->success();

            if (!$inserted || !$entry_id = $db->getInsertID()) {
                throw new DatabaseException('Could not insert in the entries table.');
            }

            // Iterate over all data for this entry
            foreach ($entry->getData() as $field_id => $field) {
                // Write data
                static::saveFieldData($entry_id, $field_id, $field);
            }

            $entry->set('id', $entry_id);
        })->execute()->success();
    }

    /**
     * Update an existing Entry object given an Entry object
     *
     * @see EntryManager::saveFieldData()
     * @param Entry $entry
     *  An Entry object
     * @throws DatabaseException
     * @return boolean
     */
    public static function edit(Entry $entry)
    {
        return Symphony::Database()->transaction(function (Database $db) use ($entry) {
            // Update modification date and modification author.
            $updated = $db
                ->update('tbl_entries')
                ->set([
                    'modification_author_id' => $entry->get('modification_author_id'),
                    'modification_date' => $entry->get('modification_date'),
                    'modification_date_gmt' => $entry->get('modification_date_gmt')
                ])
                ->where(['id' => $entry->get('id')])
                ->execute()
                ->success();

            if (!$updated) {
                throw new DatabaseException('Could not update the entries table.');
            }

            // Iterate over all data for this entry
            foreach ($entry->getData() as $field_id => $field) {
                // Write data
                static::saveFieldData($entry->get('id'), $field_id, $field);
            }
        })->execute()->success();
    }

    /**
     * Given an Entry ID, or an array of Entry ID's, delete all
     * data associated with this Entry using a Field's `entryDataCleanup()`
     * function, and then remove this Entry from `tbl_entries`. If the `$entries`
     * all belong to the same section, passing `$section_id` will improve
     * performance
     *
     * @param array|integer $entries
     *  An entry_id, or an array of entry id's to delete
     * @param integer $section_id (optional)
     *  If possible, the `$section_id` of the the `$entries`. This parameter
     *  should be left as null if the `$entries` array contains entry_id's for
     *  multiple sections.
     * @throws DatabaseException
     * @throws Exception
     * @return boolean
     */
    public static function delete($entries, $section_id = null)
    {
        $needs_data = true;

        if (!is_array($entries)) {
            $entries = array($entries);
        }

        // Get the section's schema
        if (!is_null($section_id)) {
            $section = (new SectionManager)->select()->section($section_id)->execute()->next();

            if ($section instanceof Section) {
                $fields = $section->fetchFields();
                $data = array();

                foreach ($fields as $field) {
                    $reflection = new ReflectionClass($field);
                    // This field overrides the default implementation, so pass it data.
                    $data[$field->get('element_name')] = $reflection->getMethod('entryDataCleanup')->class == 'Field' ? false : true;
                }

                $data = array_filter($data);

                if (empty($data)) {
                    $needs_data = false;
                }
            }
        }

        // We'll split $entries into blocks of 2500 (random number)
        // and process the deletion in chunks.
        $chunks = array_chunk($entries, 2500);

        foreach ($chunks as $chunk) {
            // If we weren't given a `section_id` we'll have to process individually
            // If we don't need data for any field, we can process the whole chunk
            // without building Entry objects, otherwise we'll need to build
            // Entry objects with data
            if (is_null($section_id) || !$needs_data) {
                $entries = $chunk;
            } elseif ($needs_data) {
                $entries = (new EntryManager)
                    ->select()
                    ->entries($chunk)
                    ->section($section_id)
                    ->includeAllFields()
                    ->disableDefaultSort()
                    ->execute()
                    ->rows();
            }

            if ($needs_data) {
                foreach ($entries as $id) {
                    // Handles the case where `section_id` was not provided
                    if (is_null($section_id)) {
                        $e = (new EntryManager)->select()->entry($id)->execute()->next();

                        if (!$e) {
                            continue;
                        }

                        $e = (new EntryManager)
                            ->select()
                            ->entry($id)
                            ->section($e->get('section_id'))
                            ->includeAllFields()
                            ->disableDefaultSort()
                            ->execute()
                            ->next();

                        // If we needed data, whole Entry objects will exist
                    } elseif ($needs_data) {
                        $e = $id;
                        $id = $e->get('id');
                    }

                    // Time to loop over it and send it to the fields.
                    // Note we can't rely on the `$fields` array as we may
                    // also be dealing with the case where `section_id` hasn't
                    // been provided
                    $entry_data = $e->getData();

                    foreach ($entry_data as $field_id => $data) {
                        $field = (new FieldManager)->select()->field($field_id)->execute()->next();
                        $field->entryDataCleanup($id, $data);
                    }
                }
            } else {
                foreach ($fields as $field) {
                    $field->entryDataCleanup($chunk);
                }
            }

            Symphony::Database()
                ->delete('tbl_entries')
                ->where(['id' => ['in' => $chunk]])
                ->execute();
        }

        return true;
    }

    /**
     * This function will return an array of Entry objects given an ID or an array of ID's.
     * Do not provide `$entry_id` as an array if not specifying the `$section_id`. This function
     * is commonly passed custom SQL statements through the `$where` and `$join` parameters
     * that is generated by the fields of this section.
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select() instead
     * @since Symphony 2.7.0 it will also call a new method on fields,
     * `buildSortingSelectSQL()`, to make sure fields can add ordering columns in
     * the SELECT clause. This is required on MySQL 5.7+ strict mode.
     *
     * @param integer|array $entry_id
     *  An array of Entry ID's or an Entry ID to return
     * @param integer $section_id
     *  The ID of the Section that these entries are contained in
     * @param integer $limit
     *  The limit of entries to return
     * @param integer $start
     *  The starting offset of the entries to return
     * @param string $where
     *  Any custom WHERE clauses. The tbl_entries alias is `e`
     * @param string $joins
     *  Any custom JOIN's
     * @param boolean $group
     *  Whether the entries need to be grouped by Entry ID or not
     * @param boolean $buildentries
     *  Whether to return an array of entry ID's or Entry objects. Defaults to
     *  true, which will return Entry objects
     * @param array $element_names
     *  Choose whether to get data from a subset of fields or all fields in a section,
     *  by providing an array of field names. Defaults to null, which will load data
     *  from all fields in a section.
     * @param boolean $enable_sort
     *  Defaults to true, if false this function will not apply any sorting
     * @throws Exception
     * @return array
     *  If `$buildentries` is true, this function will return an array of Entry objects,
     *  otherwise it will return an associative array of Entry data from `tbl_entries`
     */
    public static function fetch($entry_id = null, $section_id = null, $limit = null, $start = null, $where = null, $joins = null, $group = false, $buildentries = true, $element_names = null, $enable_sort = true)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog('EntryManager::fetch()', 'EntryManager::select()');
        }

        if (!$entry_id && !$section_id) {
            return [];
        }

        if (!$section_id) {
            $section_id = self::fetchEntrySectionID($entry_id);
        }

        $section = (new SectionManager)->select()->section($section_id)->execute()->next();
        if (!is_object($section)) {
            return [];
        }

        $query = (new EntryManager)->select();

        if ($group) {
            $query->distinct();
        }

        // A single $entry_id doesn't need to be sorted on, or if it's explicitly disabled
        if ((!is_array($entry_id) && General::intval($entry_id) > 0) || !$enable_sort) {
            $query->disableDefaultSort();
        } elseif (self::$_fetchSortField) {
            $query->sort((string)self::$_fetchSortField, self::$_fetchSortDirection);
        }

        if ($entry_id && !is_array($entry_id)) {
            // The entry ID may be a comma-separated string, so explode it to make it
            // a proper array
            $entry_id = explode(',', $entry_id);
        }

        // An existing entry ID will be an array now, and we can force integer values
        if ($entry_id) {
            $entry_id = array_map(array('General', 'intval'), $entry_id);
        }
        if (!empty($entry_id)) {
            $query->entries($entry_id);
        }
        if ($limit) {
            $query->limit($limit);
        }
        if ($start) {
            $query->offset($start);
        }
        if ($element_names === null) {
            $element_names = array_map(function ($field) {
                return $field['element_name'];
            }, $section->fetchFieldsSchema());
        }
        if ($buildentries && is_array($element_names)) {
            $query->schema($element_names);
        }
        if ($joins) {
            $joins = $query->replaceTablePrefix($joins);
            $query->unsafeAppendSQLPart('join', $joins);
        }
        if ($where) {
            $where = $query->replaceTablePrefix($where);
            // Ugly hack: mysqli allowed this....
            $where = str_replace('IN ()', 'IN (0)', $where);
            $query->unsafe()->unsafeAppendSQLPart('where', "WHERE 1=1 $where");
        }
        $query->section($section_id);

        return $query->execute()->rows();
    }

    /**
     * Given an Entry ID, return the Section ID that it belongs to
     *
     * @param integer $entry_id
     *  The ID of the Entry to return it's section
     * @return integer
     *  The Section ID for this Entry's section
     */
    public static function fetchEntrySectionID($entry_id)
    {
        return (new EntryManager)
            ->select()
            ->entry($entry_id)
            ->limit(1)
            ->execute()
            ->integer('section_id');
    }

    /**
     * Return the count of the number of entries in a particular section.
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select() instead
     * @param integer $section_id
     *  The ID of the Section where the Entries are to be counted
     * @param string $where
     *  Any custom WHERE clauses
     * @param string $joins
     *  Any custom JOIN's
     * @param boolean $group
     *  Whether the entries need to be grouped by Entry ID or not
     * @return integer
     */
    public static function fetchCount($section_id = null, $where = null, $joins = null, $group = false)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog('EntryManager::fetchCount()', 'EntryManager::select()->count()');
        }

        if (is_null($section_id)) {
            return false;
        }

        $section = (new SectionManager)->select()->section($section_id)->execute()->next();

        if (!is_object($section)) {
            return false;
        }

        $sql = (new EntryManager)->select()->count()->section($section_id);

        if ($group) {
            $sql->distinct();
        }
        if ($joins) {
            $joins = $sql->replaceTablePrefix($joins);
            $sql->unsafeAppendSQLPart('join', $joins);
        }
        if ($where) {
            $where = $sql->replaceTablePrefix($where);
            // Ugly hack: mysqli allowed this....
            $where = str_replace('IN ()', 'IN (0)', $where);
            $sql->unsafe()->unsafeAppendSQLPart('where', $where);
        }

        return $sql->execute()->integer(0);
    }

    /**
     * Returns an array of Entry objects, with some basic pagination given
     * the number of Entry's to return and the current starting offset. This
     * function in turn calls the fetch function that does alot of the heavy
     * lifting. For instance, if there are 60 entries in a section and the pagination
     * dictates that per page, 15 entries are to be returned, by passing 2 to
     * the $page parameter you could return entries 15-30
     *
     * @deprecated @since Symphony 3.0.0
     *  Use select() instead
     * @param integer $page
     *  The page to return, defaults to 1
     * @param integer $section_id
     *  The ID of the Section that these entries are contained in
     * @param integer $entriesPerPage
     *  The number of entries to return per page.
     * @param string $where
     *  Any custom WHERE clauses
     * @param string $joins
     *  Any custom JOIN's
     * @param boolean $group
     *  Whether the entries need to be grouped by Entry ID or not
     * @param boolean $records_only
     *  If this is set to true, an array of Entry objects will be returned
     *  without any basic pagination information. Defaults to false
     * @param boolean $buildentries
     *  Whether to return an array of entry ID's or Entry objects. Defaults to
     *  true, which will return Entry objects
     * @param array $element_names
     *  Choose whether to get data from a subset of fields or all fields in a section,
     *  by providing an array of field names. Defaults to null, which will load data
     *  from all fields in a section.
     * @throws Exception
     * @return array
     *  Either an array of Entry objects, or an associative array containing
     *  the total entries, the start position, the entries per page and the
     *  Entry objects
     */
    public static function fetchByPage($page = 1, $section_id, $entriesPerPage, $where = null, $joins = null, $group = false, $records_only = false, $buildentries = true, array $element_names = null)
    {
        if (Symphony::Log()) {
            Symphony::Log()->pushDeprecateWarningToLog(
                'EntryManager::fetchByPage()',
                'EntryManager::select()->paginate()'
            );
        }

        if ($entriesPerPage != null && !is_string($entriesPerPage) && !is_numeric($entriesPerPage)) {
            throw new Exception(__('Entry limit specified was not a valid type. String or Integer expected.'));
        } elseif ($entriesPerPage == null) {
            $records = self::fetch(null, $section_id, null, null, $where, $joins, $group, $buildentries, $element_names);

            $count = self::fetchCount($section_id, $where, $joins, $group);

            $entries = array(
                'total-entries' => $count,
                'total-pages' => 1,
                'remaining-pages' => 0,
                'remaining-entries' => 0,
                'start' => 1,
                'limit' => $count,
                'records' => $records
            );

            return $entries;
        } else {
            $start = (max(1, $page) - 1) * $entriesPerPage;

            $records = ($entriesPerPage == '0' ? null : self::fetch(null, $section_id, $entriesPerPage, $start, $where, $joins, $group, $buildentries, $element_names));

            if ($records_only) {
                return array('records' => $records);
            }

            $entries = array(
                'total-entries' => self::fetchCount($section_id, $where, $joins, $group),
                'records' => $records,
                'start' => max(1, $start),
                'limit' => $entriesPerPage
            );

            $entries['remaining-entries'] = max(0, $entries['total-entries'] - ($start + $entriesPerPage));
            $entries['total-pages'] = max(1, ceil($entries['total-entries'] * (1 / $entriesPerPage)));
            $entries['remaining-pages'] = max(0, $entries['total-pages'] - $page);

            return $entries;
        }
    }

    /**
     * Creates a new Entry object using this class as the parent.
     *
     * @return Entry
     */
    public static function create()
    {
        return new Entry;
    }

    /**
     * Factory method that creates a new EntryQuery.
     *
     * @since Symphony 3.0.0
     * @param array $values
     *  The fields to select. By default it's none of them, so the query
     *  only populates the object with its data.
     * @return EntryQuery
     */
    public function select(array $schema = [])
    {
        return new EntryQuery(Symphony::Database(), $schema);
    }
}