awsmug/torro-forms

View on GitHub
src/db-objects/submissions/submission.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/**
 * Submission class
 *
 * @package TorroForms
 * @since 1.0.0
 */

namespace awsmug\Torro_Forms\DB_Objects\Submissions;

use Leaves_And_Love\Plugin_Lib\DB_Objects\Model;
use Leaves_And_Love\Plugin_Lib\DB_Objects\Traits\Sitewide_Model_Trait;
use awsmug\Torro_Forms\DB_Objects\Forms\Form;
use awsmug\Torro_Forms\DB_Objects\Containers\Container;
use WP_Error;

/**
 * Class representing a submission.
 *
 * @since 1.0.0
 *
 * @property int    $form_id
 * @property int    $user_id
 * @property int    $timestamp
 * @property string $remote_addr
 * @property string $user_key
 * @property string $status
 *
 * @property-read int $id
 */
class Submission extends Model {
    use Sitewide_Model_Trait;

    /**
     * Submission ID.
     *
     * @since 1.0.0
     * @var int
     */
    protected $id = 0;

    /**
     * ID of the form this submission applies to.
     *
     * @since 1.0.0
     * @var int
     */
    protected $form_id = 0;

    /**
     * User ID of the user who created this submission, if any.
     *
     * @since 1.0.0
     * @var int
     */
    protected $user_id = 0;

    /**
     * Timestamp of when the submission was created.
     *
     * @since 1.0.0
     * @var int
     */
    protected $timestamp = 0;

    /**
     * IP address of the user who created this submission.
     *
     * @since 1.0.0
     * @var string
     */
    protected $remote_addr = '';

    /**
     * Submission user key.
     *
     * @since 1.0.0
     * @var string
     */
    protected $user_key = '';

    /**
     * Submission status identifier.
     *
     * @since 1.0.0
     * @var string
     */
    protected $status = 'completed';

    /**
     * Internal submission value storage.
     *
     * @since 1.0.0
     * @var array|null
     */
    protected $values = null;

    /**
     * Internal element value storage.
     *
     * @since 1.0.0
     * @var array|null
     */
    protected $element_values = null;

    /**
     * Magic isset-er.
     *
     * Checks whether a property is set.
     *
     * @since 1.0.0
     *
     * @param string $property Property to check for.
     * @return bool True if the property is set, false otherwise.
     */
    public function __isset( $property ) {
        if ( 'values' === $property ) {
            return true;
        }

        if ( preg_match( '/^element_([0-9]+)_([a-z_]+)_value$/U', $property, $matches ) ) {
            $values = $this->get_element_values_data();

            if ( isset( $values[ $matches[1] ] ) && isset( $values[ $matches[1] ][ $matches[2] ] ) ) {
                return true;
            }

            return false;
        }

        return parent::__isset( $property );
    }

    /**
     * Magic getter.
     *
     * Returns a property value.
     *
     * @since 1.0.0
     *
     * @param string $property Property to get.
     * @return mixed Property value, or null if property is not set.
     */
    public function __get( $property ) {
        if ( 'values' === $property ) {
            if ( is_array( $this->values ) ) {
                return $this->values;
            }

            return $this->get_submission_values_data();
        }

        if ( preg_match( '/^element_([0-9]+)_([a-z_]+)_value$/U', $property, $matches ) ) {
            $values = $this->get_element_values_data();

            if ( isset( $values[ $matches[1] ] ) && isset( $values[ $matches[1] ][ $matches[2] ] ) ) {
                return $values[ $matches[1] ][ $matches[2] ];
            }

            return null;
        }

        return parent::__get( $property );
    }

    /**
     * Magic setter.
     *
     * Sets a property value.
     *
     * @since 1.0.0
     *
     * @param string $property Property to set.
     * @param mixed  $value    Property value.
     */
    public function __set( $property, $value ) {
        if ( 'values' === $property ) {
            $this->set_submission_values_data( $value );
            return;
        }

        if ( preg_match( '/^element_([0-9]+)_([a-z_]+)_value$/U', $property, $matches ) ) {
            if ( ! isset( $this->values ) ) {
                $this->values = $this->get_submission_values_data();
            }

            $original_value = $value;

            $value = (array) $value;

            $indexes_to_remove = array();

            foreach ( $this->values as $index => $item ) {
                $item['field'] = ! empty( $item['field'] ) ? $item['field'] : '_main';

                if ( (int) $matches[1] !== (int) $item['element_id'] || $matches[2] !== $item['field'] ) {
                    continue;
                }

                if ( ! empty( $value ) ) {
                    $this->values[ $index ]['value'] = array_shift( $value );
                } else {
                    $indexes_to_remove[] = $index;
                }
            }

            foreach ( $indexes_to_remove as $index_to_remove ) {
                unset( $this->values[ $index_to_remove ] );
            }

            foreach ( $value as $single_value ) {
                $this->values[] = array(
                    'id'         => 0,
                    'element_id' => (int) $matches[1],
                    'field'      => $matches[2],
                    'value'      => $single_value,
                );
            }

            $this->get_element_values_data();

            $this->element_values[ $matches[1] ][ $matches[2] ] = $original_value;
        }

        parent::__set( $property, $value );
    }

    /**
     * Returns the parent form for the submission.
     *
     * @since 1.0.0
     *
     * @return Form|null Parent form, or null if none set.
     */
    public function get_form() {
        if ( empty( $this->form_id ) ) {
            return null;
        }

        return $this->manager->get_parent_manager( 'forms' )->get( $this->form_id );
    }

    /**
     * Returns all submission values that belong to the submission.
     *
     * @since 1.0.0
     *
     * @param array $args Optional. Additional query arguments. Default empty array.
     * @return Submission_Value_Collection List of submission values.
     */
    public function get_submission_values( $args = array() ) {
        if ( empty( $this->id ) ) {
            return $this->manager->get_child_manager( 'submission_values' )->get_collection( array(), 0, 'objects' );
        }

        $args = wp_parse_args(
            $args,
            array(
                'number'        => -1,
                'submission_id' => $this->id,
            )
        );

        return $this->manager->get_child_manager( 'submission_values' )->query( $args );
    }

    /**
     * Synchronizes the model with the database by storing the currently pending values.
     *
     * If the model is new (i.e. does not have an ID yet), it will be inserted to the database.
     *
     * @since 1.0.0
     *
     * @return true|WP_Error True on success, or an error object on failure.
     */
    public function sync_upstream() {
        $result = parent::sync_upstream();

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        if ( null !== $this->values ) {
            $manager = $this->manager->get_child_manager( 'submission_values' );

            $ids = array();
            foreach ( $this->values as $item ) {
                $submission_value = null;
                if ( ! empty( $item['id'] ) ) {
                    $submission_value = $manager->get( $item['id'] );
                    if ( $submission_value && $this->id !== $submission_value->submission_id ) {
                        continue;
                    }
                }

                if ( ! $submission_value ) {
                    $submission_value = $manager->create();
                }

                $submission_value->submission_id = $this->id;
                $submission_value->field         = $item['field'];
                $submission_value->value         = $item['value'];
                if ( ! empty( $item['element_id'] ) ) {
                    $submission_value->element_id = $item['element_id'];
                }

                $submission_value->sync_upstream();

                if ( empty( $submission_value->id ) ) {
                    continue;
                }

                $ids[] = $submission_value->id;
            }

            if ( ! empty( $ids ) ) {
                $old_values = $this->get_submission_values(
                    array(
                        'exclude' => $ids,
                    )
                );
                foreach ( $old_values as $old_value ) {
                    $old_value->delete();
                }
            }

            $this->values = null;
        }

        return $result;
    }

    /**
     * Synchronizes the model with the database by fetching the currently stored values.
     *
     * If the model contains unsynchronized changes, these will be overridden. This method basically allows
     * to reset the model to the values stored in the database.
     *
     * @since 1.0.0
     *
     * @return true|WP_Error True on success, or an error object on failure.
     */
    public function sync_downstream() {
        $result = parent::sync_downstream();

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $this->values = null;

        return $result;
    }

    /**
     * Deletes the model from the database.
     *
     * @since 1.0.0
     *
     * @return true|WP_Error True on success, or an error object on failure.
     */
    public function delete() {
        $submission_values = $this->get_submission_values();
        foreach ( $submission_values as $submission_value ) {
            $submission_value->delete();
        }

        return parent::delete();
    }

    /**
     * Formats the submission date and time.
     *
     * @since 1.0.0
     *
     * @param string $format Datetime format string. Will be localized.
     * @param bool   $gmt    Optional. Whether to return as GMT. Default true.
     * @return string Formatted date and time.
     */
    public function format_datetime( $format, $gmt = true ) {
        $timestamp = $this->timestamp;
        if ( ! $gmt ) {
            $timestamp = strtotime( get_date_from_gmt( date( 'Y-m-d H:i:s', $timestamp ) ) );
        }

        return date_i18n( $format, $timestamp );
    }

    /**
     * Sets the current container for the submission.
     *
     * The form ID of the container must match the submission's form ID.
     *
     * @since 1.0.0
     *
     * @param Container|null $container Container, or null if the current container data should be unset.
     */
    public function set_current_container( $container ) {
        if ( null === $container ) {
            $this->__set( 'current_container_id', null );
            return true;
        }

        if ( $container->form_id !== $this->form_id ) {
            return false;
        }

        $this->__set( 'current_container_id', $container->id );
        return true;
    }

    /**
     * Returns the current container for the submission.
     *
     * @since 1.0.0
     *
     * @return Container|null Current container, or null on failure.
     */
    public function get_current_container() {
        $container_id = (int) $this->__get( 'current_container_id' );

        if ( ! empty( $container_id ) ) {
            $container = $this->manager->get_parent_manager( 'forms' )->get_child_manager( 'containers' )->get( $container_id );
        } else {
            $form = $this->get_form();
            if ( ! $form ) {
                return null;
            }

            $container_collection = $form->get_containers(
                array(
                    'number'        => 1,
                    'orderby'       => array(
                        'sort' => 'ASC',
                    ),
                    'no_found_rows' => true,
                )
            );
            if ( 1 > count( $container_collection ) ) {
                return null;
            }

            $container = $container_collection[0];
            $this->set_current_container( $container );
        }

        return $container;
    }

    /**
     * Returns the next container for the submission, if there is one.
     *
     * @since 1.0.0
     *
     * @return Container|null Next container, or null if there is none.
     */
    public function get_next_container() {
        $container = $this->get_current_container();
        if ( ! $container ) {
            return null;
        }

        $form = $this->get_form();
        if ( ! $form ) {
            return null;
        }

        $container_collection = $form->get_containers(
            array(
                'number'        => 1,
                'sort'          => array(
                    'greater_than' => $container->sort,
                ),
                'orderby'       => array(
                    'sort' => 'ASC',
                ),
                'no_found_rows' => true,
            )
        );
        if ( 1 > count( $container_collection ) ) {
            return null;
        }

        return $container_collection[0];
    }

    /**
     * Returns the previous container for the submission, if there is one.
     *
     * @since 1.0.0
     *
     * @return Container|null Previous container, or null if there is none.
     */
    public function get_previous_container() {
        $container = $this->get_current_container();
        if ( ! $container ) {
            return null;
        }

        $form = $this->get_form();
        if ( ! $form ) {
            return null;
        }

        $container_collection = $form->get_containers(
            array(
                'number'        => 1,
                'sort'          => array(
                    'lower_than' => $container->sort,
                ),
                'orderby'       => array(
                    'sort' => 'DESC',
                ),
                'no_found_rows' => true,
            )
        );
        if ( 1 > count( $container_collection ) ) {
            return null;
        }

        return $container_collection[0];
    }

    /**
     * Gets all element values set for the submission.
     *
     * The returned element values data array is an multi-dimensional associative array
     * where the keys are element IDs and their inner keys field slugs belonging to the element
     * with the actual value for the element and field combination as value.
     *
     * @since 1.0.0
     *
     * @return array Element values data set for the submission.
     */
    public function get_element_values_data() {
        if ( ! $this->primary_property_value() ) {
            return array();
        }

        if ( ! isset( $this->element_values ) ) {
            $this->element_values = $this->manager->get_element_values_data_for_submission( $this );
        }

        return $this->element_values;
    }

    /**
     * Adds an error to the submission.
     *
     * @since 1.0.0
     *
     * @param int    $element_id Element ID to add the error for.
     * @param string $code       Error code.
     * @param string $message    Error message.
     * @return bool True on success, false on failure.
     */
    public function add_error( $element_id, $code, $message ) {
        if ( ! array_key_exists( 'errors', $this->pending_meta ) ) {
            if ( $this->primary_property_value() ) {
                $this->pending_meta['errors'] = $this->manager->get_meta( $this->primary_property_value(), 'errors', true );
            } else {
                $this->pending_meta['errors'] = array();
            }
        }

        if ( ! is_array( $this->pending_meta['errors'] ) ) {
            $this->pending_meta['errors'] = array();
        }

        if ( ! isset( $this->pending_meta['errors'][ $element_id ] ) || ! is_array( $this->pending_meta['errors'][ $element_id ] ) ) {
            $this->pending_meta['errors'][ $element_id ] = array();
        }

        $this->pending_meta['errors'][ $element_id ][ $code ] = $message;

        return true;
    }

    /**
     * Removes an error from the submission.
     *
     * @since 1.0.0
     *
     * @param int    $element_id Element ID to remove an error for.
     * @param string $code       Error code to remove.
     * @return bool True on success, false on failure.
     */
    public function remove_error( $element_id, $code ) {
        if ( ! array_key_exists( 'errors', $this->pending_meta ) ) {
            if ( $this->primary_property_value() ) {
                $this->pending_meta['errors'] = $this->manager->get_meta( $this->primary_property_value(), 'errors', true );
            } else {
                $this->pending_meta['errors'] = array();
            }
        }

        if ( ! is_array( $this->pending_meta['errors'] ) ) {
            return false;
        }

        if ( ! is_array( $this->pending_meta['errors'][ $element_id ] ) ) {
            return false;
        }

        if ( ! isset( $this->pending_meta['errors'][ $element_id ][ $code ] ) ) {
            return false;
        }

        unset( $this->pending_meta['errors'][ $element_id ][ $code ] );

        if ( empty( $this->pending_meta['errors'][ $element_id ] ) ) {
            unset( $this->pending_meta['errors'][ $element_id ] );
        }

        if ( empty( $this->pending_meta['errors'] ) ) {
            $this->pending_meta['errors'] = null;
        }

        return true;
    }

    /**
     * Gets all errors, for the entire submission or a specific element.
     *
     * @since 1.0.0
     *
     * @param int|null $element_id Optional. If an element ID is given, only errors for that element are returned.
     * @return array If $element_id is given, the array of `$code => $message` pairs is returned. Otherwise the array
     *               of `$element_id => $errors` pairs is returned.
     */
    public function get_errors( $element_id = null ) {
        if ( ! array_key_exists( 'errors', $this->pending_meta ) ) {
            if ( ! $this->primary_property_value() ) {
                return array();
            }

            $errors = $this->manager->get_meta( $this->primary_property_value(), 'errors', true );
        } else {
            $errors = $this->pending_meta['errors'];
        }

        if ( ! is_array( $errors ) ) {
            return array();
        }

        if ( null !== $element_id ) {
            if ( empty( $errors[ $element_id ] ) ) {
                return array();
            }

            return $errors[ $element_id ];
        }

        return $errors;
    }

    /**
     * Checks whether there are errors, for the entire submission or a specific element.
     *
     * @since 1.0.0
     *
     * @param int|null $element_id Optional. If an element ID is given, only errors for that element are returned.
     * @return bool True if there are errors, false otherwise.
     */
    public function has_errors( $element_id = null ) {
        $errors = $this->get_errors( $element_id );

        return ! empty( $errors );
    }

    /**
     * Resets all errors, for the entire submission or a specific element.
     *
     * @since 1.0.0
     *
     * @param int|null $element_id Optional. If an element ID is given, only errors for that element are reset.
     * @return bool True on success, false on failure.
     */
    public function reset_errors( $element_id = null ) {
        if ( ! array_key_exists( 'errors', $this->pending_meta ) ) {
            if ( ! $this->primary_property_value() ) {
                return false;
            }

            $this->pending_meta['errors'] = $this->manager->get_meta( $this->primary_property_value(), 'errors', true );
        }

        if ( ! is_array( $this->pending_meta['errors'] ) ) {
            if ( null !== $this->pending_meta['errors'] ) {
                unset( $this->pending_meta['errors'] );
            }

            return false;
        }

        if ( null !== $element_id ) {
            if ( ! is_array( $this->pending_meta['errors'] ) || empty( $this->pending_meta['errors'][ $element_id ] ) ) {
                return false;
            }

            unset( $this->pending_meta['errors'][ $element_id ] );

            return true;
        }

        $this->pending_meta['errors'] = null;

        return true;
    }

    /**
     * Gets submission values data for the submission, to be used with the field manager.
     *
     * @since 1.0.0
     *
     * @return array Submission values data.
     */
    protected function get_submission_values_data() {
        $data = array();

        foreach ( $this->get_submission_values() as $submission_value ) {
            $data[] = array(
                'id'         => $submission_value->id,
                'element_id' => $submission_value->element_id,
                'field'      => $submission_value->field,
                'value'      => $submission_value->value,
            );
        }

        return $data;
    }

    /**
     * Sets submission values data for the submission, to be used with the field manager.
     *
     * @since 1.0.0
     *
     * @param array $value Submission values data.
     */
    protected function set_submission_values_data( $value ) {
        if ( ! is_array( $value ) ) {
            return;
        }

        $data = array();
        foreach ( $value as $item ) {
            $data[] = array(
                'id'         => ! empty( $item['id'] ) ? (int) $item['id'] : 0,
                'element_id' => ! empty( $item['element_id'] ) ? (int) $item['element_id'] : 0,
                'field'      => ! empty( $item['field'] ) ? sanitize_key( $item['field'] ) : '',
                'value'      => ! empty( $item['value'] ) ? $item['value'] : '',
            );
        }

        $this->values = $data;
    }

    /**
     * Returns a list of internal properties that are not publicly accessible.
     *
     * When overriding this method, always make sure to merge with the parent result.
     *
     * @since 1.0.0
     *
     * @return array Property blacklist.
     */
    protected function get_blacklist() {
        $blacklist = parent::get_blacklist();

        $blacklist[] = 'values';
        $blacklist[] = 'element_values';

        return $blacklist;
    }
}