intraxia/wp-gistpen

View on GitHub
app/Database/Repository/WordPressCustomTable.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace Intraxia\Gistpen\Database\Repository;

use Intraxia\Gistpen\Model\Run;
use Intraxia\Jaxion\Axolotl\Model;
use Intraxia\Jaxion\Axolotl\Collection;
use Intraxia\Jaxion\Contract\Axolotl\UsesCustomTable;
use WP_Error;

/**
 * Service for managing Models backed by a custom table.
 */
class WordPressCustomTable extends AbstractRepository {

    /**
     * Get a single model of the provided class with the given ID.
     *
     * @param string $class  Fully qualified class name of model.
     * @param int    $id     ID of the model.
     * @param array  $params Extra params/options.
     *
     * @return Model|UsesCustomTable|WP_Error
     */
    public function find( $class, $id, array $params = array() ) {
        global $wpdb;

        $result = $wpdb->get_row(
            $wpdb->prepare(
                // @codingStandardsIgnoreStart
                "
                    SELECT * FROM {$this->em->make_table_name( $class )}
                    WHERE {$class::get_primary_key()} = %d
                ",
                // @codingStandardsIgnoreEnd
                $id
            ),
            ARRAY_A
        );

        if ( ! $result ) {
            return new WP_Error(
                'db_error',
                sprintf(
                    /* translators: %s: Query error. */
                    __( 'Query failed with error: %s', 'wp-gistpen' ),
                    $wpdb->last_error
                )
            );
        }

        if ( isset( $result['items'] ) ) {
            $result['items'] = maybe_unserialize( $result['items'] );
        }

        $model = new $class( $result );
        $model->sync_original();

        return $model;
    }

    /**
     * Finds all the models of the provided class for the given params.
     *
     * This method will return an empty Collection if the query returns no models.
     *
     * @param string $class  Fully qualified class name of models to find.
     * @param array  $params Params to constrain the find.
     *
     * @return Collection|WP_Error
     */
    public function find_by( $class, array $params = array() ) {
        global $wpdb;
        $models = new Collection( $class );

        $query = "SELECT * FROM {$this->em->make_table_name( $class )}";

        foreach ( $params as $key => $value ) {
            switch ( $key ) {
                case 'order_by':
                    // skip, handled below
                    break;
                default:
                    if ( $this->is_valid_key( $class, $key ) ) {
                        $query .= $wpdb->prepare(
                            " WHERE {$key} = %s",  // @codingStandardsIgnoreLine
                            $value
                        );
                    }
            }
        }

        if ( isset( $params['order_by'] ) ) {
            // @todo find a better way of whitelisting order_by
            switch ( $params['order_by'] ) {
                case 'ID':
                    $query .= " ORDER BY {$params['order_by']} DESC";
                    break;
            }
        }

        $results = $wpdb->get_results( $query, ARRAY_A );  // @codingStandardsIgnoreLine

        if ( ! is_array( $results ) ) {
            return new WP_Error(
                'db_error',
                sprintf(
                    /* translators: %s: Query error. */
                    __( 'Query failed with error: %s', 'wp-gistpen' ),
                    $wpdb->last_error
                )
            );
        }

        foreach ( $results as $result ) {
            if ( isset( $result['items'] ) ) {
                $result['items'] = maybe_unserialize( $result['items'] );
            }

            $model = new $class( $result );
            $model->sync_original();

            $models = $models->add( $model );
        }

        return $models;
    }

    /**
     * Saves a new model of the provided class with the given data.
     *
     * @param string $class
     * @param array  $data
     * @param array  $options
     *
     * @return Model|WP_Error
     */
    public function create( $class, array $data = array(), array $options = array() ) {
        global $wpdb;

        $data = $this->validate_data( $class, $data );

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

        $results = $wpdb->insert(
            $this->em->make_table_name( $class ),
            // @todo escape values
            // this is only acceptable because user input never gets in here right now
            $data
        );

        if ( ! $results ) {
            return new WP_Error(
                'db_error',
                sprintf(
                    /* translators: %s: Query error. */
                    __( 'Query failed with error: %s', 'wp-gistpen' ),
                    $wpdb->last_error
                )
            );
        }

        return $this->find( $class, $wpdb->insert_id );
    }

    /**
     * Updates a model with its latest data.
     *
     * @param Model|UsesCustomTable $model
     *
     * @return Model|WP_Error
     */
    public function persist( Model $model ) {
        global $wpdb;

        $class = get_class( $model );

        if ( ! $model->get_primary_id() ) {
            return $this->create( $class, $model->get_table_attributes() );
        }

        $valid = $this->validate_data( $class, $model->get_table_attributes() );

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

        $results = $wpdb->update(
            $this->em->make_table_name( $class ),
            $valid,
            // @todo only update changed attributes
            /** UsesCustomTable $model */
            array( $model->get_primary_key() => $model->get_primary_id() )
        );

        if ( ! $results ) {
            return new WP_Error(
                'db_error',
                sprintf(
                    /* translators: %s: Query error. */
                    __( 'Query failed with error: %s', 'wp-gistpen' ),
                    $wpdb->last_error
                )
            );
        }

        return $this->find( $class, $model->get_primary_id() );
    }

    /**
     * Delete the provided model from the database.
     *
     * @param Model|UsesCustomTable $model
     * @param bool                  $force
     *
     * @return mixed
     */
    public function delete( Model $model, $force = false ) {
        global $wpdb;

        $results = $wpdb->delete(
            $this->em->make_table_name( $model ),
            array( $model->get_primary_key() => $model->get_primary_id() )
        );

        if ( ! $results ) {
            return new WP_Error(
                'db_error',
                sprintf(
                    /* translators: %s: Query error. */
                    __( 'Query failed with error: %s', 'wp-gistpen' ),
                    $wpdb->last_error
                )
            );
        }

        if ( $model instanceof Run ) {
            $results = $wpdb->delete(
                $this->em->make_table_name( \Intraxia\Gistpen\Model\Message::class ),
                array( 'run_id' => $model->get_primary_id() )
            );

            if ( ! $results ) {
                return new WP_Error(
                    'db_error',
                    sprintf(
                        /* translators: %s: Query error. */
                        __( 'Query failed with error: %s', 'wp-gistpen' ),
                        $wpdb->last_error
                    )
                );
            }
        }

        return $model;
    }

    /**
     * Checks whether the given key is valid for a given class.
     *
     * @param string $class Class to check against.
     * @param string $key   Key to validate.
     *
     * @return bool
     */
    private function is_valid_key( $class, $key ) {
        switch ( $class ) {
            case \Intraxia\Gistpen\Model\Message::class:
                return in_array( $key, array( 'run_id' ), true );
            case \Intraxia\Gistpen\Model\Run::class:
                return in_array( $key, array( 'job', 'status' ), true );
            default:
                return false;
        }
    }

    /**
     * Validate a classes array of data.
     *
     * @param string $class
     * @param array  $data
     *
     * @return WP_Error|array
     */
    private function validate_data( $class, $data ) {
        switch ( $class ) {
            case \Intraxia\Gistpen\Model\Run::class:
                if ( isset( $data['items'] ) ) {
                    $data['items'] = maybe_serialize( $data['items'] );
                }

                return $data;
            case \Intraxia\Gistpen\Model\Message::class:
                if ( ! isset( $data['run_id'] ) ) {
                    return new WP_Error(
                        'invalid_run_id',
                        __( 'run_id was not provided' )
                    );
                }

                global $wpdb;

                $count = (int) $wpdb->get_var(
                    $wpdb->prepare(
                         // @codingStandardsIgnoreStart
                        "
                            SELECT COUNT(*) FROM {$this->em->make_table_name( \Intraxia\Gistpen\Model\Run::class )}
                            WHERE ID = %d
                        ",
                         // @codingStandardsIgnoreEnd
                        $data['run_id']
                    )
                );

                if ( ! $count ) {
                    return new WP_Error(
                        'invalid_data',
                        sprintf(
                            /* translators: %s: Run ID. */
                            __( 'run_id %s is invalid', 'wp-gistpen' ),
                            $data['run_id']
                        )
                    );
                }

                return $data;
            default:
                return $data;
        }
    }
}