spiritix/lada-cache

View on GitHub
src/Spiritix/LadaCache/Reflector.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
94%
<?php
/**
 * This file is part of the spiritix/lada-cache package.
 *
 * @copyright Copyright (c) Matthias Isler <mi@matthias-isler.ch>
 * @license   MIT
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Spiritix\LadaCache;

use RuntimeException;
use Spiritix\LadaCache\Database\QueryBuilder;

/**
 * Query builder reflector provides information about an Eloquent query builder object.
 *
 * @package Spiritix\LadaCache\Reflector
 * @author  Matthias Isler <mi@matthias-isler.ch>
 */
class Reflector
{
    /**
     * Query of type "SELECT".
     */
    const QUERY_TYPE_SELECT = 'select';

    /**
     * Query of type "INSERT".
     */
    const QUERY_TYPE_INSERT = 'insert';

    /**
     * Query of type "UPDATE".
     */
    const QUERY_TYPE_UPDATE = 'update';

    /**
     * Query of type "DELETE".
     */
    const QUERY_TYPE_DELETE = 'delete';

    /**
     * Query of type "TRUNCATE".
     */
    const QUERY_TYPE_TRUNCATE = 'truncate';

    /**
     * Query builder instance.
     *
     * @var QueryBuilder
     */
    protected $queryBuilder;

    /**
     * The SQL operation being performed.
     *
     * @var string
     */
    private $sqlOperation;

    /**
     * The values to be saved.
     *
     * @var array
     */
    private $values = [];

    /**
     * Initialize reflector.
     *
     * @param QueryBuilder $queryBuilder
     * @param string       $sqlOperation The SQL operation to be performed
     * @param array        $values       The values to be saved
     */
    public function __construct(QueryBuilder $queryBuilder, $sqlOperation = self::QUERY_TYPE_SELECT, $values = [])
    {
        $this->queryBuilder = $queryBuilder;
        $this->sqlOperation = $sqlOperation;
        $this->values = $values;
    }

    /**
     * Returns the database name.
     *
     * @return string
     */
    public function getDatabase()
    {
        return $this->queryBuilder
            ->getConnection()
            ->getDatabaseName();
    }

    /**
     * Returns all affected tables, including joined ones.
     *
     * @return array
     */
    public function getTables()
    {
        // Get main table
        $tables = [$this->queryBuilder->from];

        // Add possible join tables
        $joins = $this->queryBuilder->joins ?: [];
        foreach ($joins as $join) {

            if (!in_array($join->table, $tables) && is_string($join->table)) {
                $tables[] = $join->table;
            }
        }

        $this->getTablesFromWhere($this->queryBuilder, $tables);

        return $tables;
    }

    /**
     * Get Table Names From Where Exists, Not Exists (whereHas/whereDoesnthave builder syntax)
     *
     * @param QueryBuilder $queryBuilder
     */
    private function getTablesFromWhere(QueryBuilder $queryBuilder, &$tables) {
        if (!isset($queryBuilder->wheres)) {
            return;
        }
        $wheres = $queryBuilder->wheres ?: [];
        foreach ($wheres as $where) {
            if ($where['type'] == 'Exists' || $where['type'] == 'NotExists') {
                $tables[] = $where['query']->from;

                // Add possible join tables
                $joins = $where['query']->joins ?: [];
                foreach ($joins as $join) {

                    if (!in_array($join->table, $tables) && is_string($join->table)) {
                        $tables[] = $join->table;
                    }
                }
            }
            if (isset($where['query'])) {
                $this->getTablesFromWhere($where['query'], $tables);
            }
        }
    }

    /**
     * Returns all affected rows as a multidimensional array, split up by table.
     *
     * @return array
     */
    public function getRows()
    {
        $rows = [];
        $wheres = $this->queryBuilder->wheres ?: [];

        foreach ($wheres as $where) {

            // Skip unsupported clauses
            if (!isset($where['column'])) {
                continue;
            }

            // If it doesn't contain the table name assume it's the "FROM" table
            if (strpos($where['column'], '.') === false) {
                $where['column'] = implode('.', [$this->queryBuilder->from, $where['column']]);
            }

            list($table, $column) = $this->splitTableAndColumn($where['column']);

            // Make sure that the where clause applies for the primary key column
            if ($column !== $this->queryBuilder->getPrimaryKeyName()) {
                continue;
            }

            // Initialize a set for the current table
            if (!isset($rows[$table])) {
                $rows[$table] = [];
            }

            // Add the rows to the current table set
            if ($where['type'] === 'Basic') {
                if ($where['operator'] === '=' && is_numeric($where['value'])) {
                    $rows[$table][] = $where['value'];
                }
            } else if ($where['type'] === 'In') {
                $rows[$table] += $where['values'];
            }
        }

        return $rows;
    }

    /**
     * Returns the type of the query.
     *
     * @return string
     */
    public function getType()
    {
        $sql = $this->getSql();

        $type = strtok(strtolower(trim($sql)), ' ');
        $type = preg_replace('/[^a-z]/i', '', $type);

        $allowedTypes = [
            self::QUERY_TYPE_SELECT,
            self::QUERY_TYPE_INSERT,
            self::QUERY_TYPE_UPDATE,
            self::QUERY_TYPE_DELETE,
            self::QUERY_TYPE_TRUNCATE,
        ];

        if (!in_array($type, $allowedTypes)) {
            throw new RuntimeException('Invalid query type');
        }

        return $type;
    }

    /**
     * Returns the compiled SQL string.
     *
     * @return string
     */
    public function getSql()
    {
        $compileFunction = $this->getCompileFunction();
        $grammar = $this->queryBuilder->getGrammar();

        $sql = call_user_func_array([$grammar, $compileFunction], array_values([
            'builder'  => $this->queryBuilder,
            'values'   => $this->values,
            'sequence' => '',
        ]));

        // For some DBMS like SQLite Laravel issues two queries as an array instead of one string
        // This is seriously not ok, but anyway...
        if (is_array($sql)) {
            $sql = implode('; ', array_keys($sql));
        }

        return $sql;
    }

    /**
     * Returns the query parameters.
     *
     * @return array
     */
    public function getParameters()
    {
        return $this->queryBuilder->getBindings();
    }

    /**
     * Splits an SQL column identifier into table and column.
     *
     * @param string $sqlString SQL column identifier
     *
     * @return array [table|null, column]
     */
    private function splitTableAndColumn($sqlString)
    {
        // Most column identifiers don't contain a database or table name
        // In this case just return what we've got
        if (strpos($sqlString, '.') === false) {
            return [null, $sqlString];
        }

        $parts = explode('.', $sqlString);

        // If we have three parts, the identifier also contains the database name$
        // Otherwise it contains table and column
        if (count($parts) === 3) {
            $table = $parts[1];
        } else {
            $table = $parts[0];
        }

        // Column is always the last part
        $column = end($parts);

        return [$table, $column];
    }

    /**
     * Get the Eloquent grammar compile function.
     *
     * @return string
     */
    private function getCompileFunction()
    {
        switch (strtolower($this->sqlOperation)) {
            case self::QUERY_TYPE_INSERT:
                return 'compileInsert';
                break;

            case 'insertgetid':
                return 'compileInsertGetId';
                break;

            case self::QUERY_TYPE_UPDATE:
                return 'compileUpdate';
                break;

            case self::QUERY_TYPE_DELETE:
                return 'compileDelete';
                break;

            case self::QUERY_TYPE_TRUNCATE:
                return 'compileTruncate';
                break;

            default:
                return 'compileSelect';
                break;
        }
    }
}