GetDKAN/sql-parser

View on GitHub
src/SqlParser.php

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
<?php

namespace SqlParser;

use Maquina\StateMachine\MachineOfMachines;
use Maquina\Builder as mb;
use Maquina\Feeder;
use Maquina\StateMachine\IStateMachine;

/**
 * SqlParser class.
 */
class SqlParser
{

    private ?IStateMachine $stateMachine = null;

    public const ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

    public const COLUMN_OR_TABLE_CHARS = self::ALPHANUM . '_-';

  /**
   * Static call for backward compatibility.
   *
   * @codeCoverageIgnore
   */
    public static function __callStatic($name, $arguments)
    {
        $instance = new static();

        return call_user_func_array([$instance, $name], $arguments);
    }

  /**
   * Validate an SQL string.
   *
   * @param string $sql
   *   An SQL statement.
   *
   * @return bool
   *   Whether the statement is valid.
   *
   * @static
   *   Can be called statically.
   */
    public function validate(string $sql): bool
    {
        $machine = $this->getMachine();
        $this->stateMachine = $machine;
        try {
            $this->feedFeeder($sql, $machine);
            return true;
        } catch (\Exception $e) {
            return false;
        }
    }

  /**
   * Get the state machine after validating a string.
   */
    public function getValidatingMachine()
    {
        return $this->stateMachine;
    }

  /**
   * Creates the state machine.
   *
   * @return \Maquina\StateMachine\IStateMachine
   *   The machine that will validate the string.
   *
   * @static
   *   Can be called statically.
   */
    public function getMachine()
    {

        $machine = $this->newMachineOfMachine('begin');
        $machine->addEndState("end");

        $machine->addMachine("select", self::getSelectMachine());
        $machine->addMachine("where", self::getWhereMachine());
        $machine->addMachine("order_by", self::getOrderByMachine());
        $machine->addMachine("limit", self::getLimitMachine());

        $machine->addTransition('begin', ["["], "select");
        $machine->addTransition('select', ["]"], "anticipation");
        $machine->addTransition('where', ["]"], "anticipation");
        $machine->addTransition('order_by', ["]"], "anticipation");
        $machine->addTransition('limit', ["]"], "anticipation");
        $machine->addTransition('anticipation', [";"], "end");
        $machine->addTransition('anticipation', ["["], "where");
        $machine->addTransition('anticipation', ["["], "order_by");
        $machine->addTransition('anticipation', ["["], "limit");

        return $machine;
    }

  /**
   * Private.
   */
    protected function getSelectMachine()
    {
        $machine = $this->newMachineOfMachine('select_start');
        $machine->addEndState("table_var");

        $machine->addMachine('select_start', mb::s('SELECT'));
        $machine->addMachine('select_var_all', mb::s("*"));
        $machine->addMachine('select_count_all', mb::s("COUNT(*)"));
        $machine->addMachine('select_var', mb::bh(
            self::COLUMN_OR_TABLE_CHARS,
            mb::ONE_OR_MORE
        ));
        $machine->addMachine('select_from', mb::s('FROM'));
        $machine->addMachine('table_var', mb::bh(
            self::COLUMN_OR_TABLE_CHARS,
            mb::ONE_OR_MORE
        ));

        $machine->addTransition('select_start', [" "], "select_var");
        $machine->addTransition('select_start', [" "], "select_var_all");
        $machine->addTransition('select_start', [" "], "select_count_all");
        $machine->addTransition('select_var', [" "], "select_from");
        $machine->addTransition('select_var', [","], "select_var");
        $machine->addTransition('select_var_all', [" "], "select_from");
        $machine->addTransition('select_count_all', [" "], "select_from");
        $machine->addTransition('select_from', [" "], "table_var");

        return $machine;
    }

  /**
   * Private.
   */
    protected function getWhereMachine()
    {
        $machine = $this->newMachineOfMachine('where_start');
        $machine->addEndState("quoted_string");

        $machine->addMachine('where_start', mb::s('WHERE'));
        $machine->addMachine('where_column', mb::bh(
            self::COLUMN_OR_TABLE_CHARS,
            mb::ONE_OR_MORE
        ));
        $machine->addMachine("equal", mb::bh("=", mb::ONE_OR_MORE));
        $machine->addMachine("quoted_string", self::getQuotedStringMachine());
        $machine->addMachine("and", mb::s("AND"));

        $machine->addTransition('where_start', [' '], "where_column");
        $machine->addTransition('where_column', [' '], "equal");
        $machine->addTransition('equal', [' '], "quoted_string");
        $machine->addTransition('quoted_string', [' '], "and");
        $machine->addTransition('and', [' '], "where_column");

        return $machine;
    }

  /**
   * Private.
   */
    protected function getQuotedStringMachine()
    {
        $machine = $this->newMachineOfMachine('start');
        $machine->addEndState("end");

        $machine->addMachine('string', mb::bh(
            self::COLUMN_OR_TABLE_CHARS . " '\\,.;:?!+-*/^=(){}[]<>`@~#$%&|",
            mb::ONE_OR_MORE
        ));

        $machine->addTransition('start', ['"'], "string");
        $machine->addTransition('string', ['"'], "end");

        return $machine;
    }

  /**
   * Private.
   */
    protected function getOrderByMachine()
    {
        $machine = $this->newMachineOfMachine('order');
        $machine->addEndState("order_asc");
        $machine->addEndState("order_desc");

        $machine->addMachine('order', mb::s('ORDER BY'));
        $machine->addMachine('order_var', mb::bh(
            self::COLUMN_OR_TABLE_CHARS,
            mb::ONE_OR_MORE
        ));
        $machine->addMachine('order_asc', mb::s('ASC'));
        $machine->addMachine('order_desc', mb::s('DESC'));

        $machine->addTransition('order', [" "], "order_var");
        $machine->addTransition('order_var', [","], "order_var");
        $machine->addTransition('order_var', [" "], "order_asc");
        $machine->addTransition('order_var', [" "], "order_desc");

        return $machine;
    }

  /**
   * Private.
   */
    protected function getLimitMachine()
    {
        $machine = $this->newMachineOfMachine('limit');
        $machine->addEndState("numeric1");
        $machine->addEndState("numeric2");
        $machine->addMachine('limit', mb::s('LIMIT'));
        $machine->addMachine('offset', mb::s('OFFSET'));
        $machine->addMachine('numeric1', mb::bh('0123456789'));
        $machine->addMachine('numeric2', mb::bh('0123456789'));

        $machine->addTransition('limit', [" "], "numeric1");
        $machine->addTransition('numeric1', [" "], "offset");
        $machine->addTransition('offset', [" "], "numeric2");

        return $machine;
    }

  /**
   * Instantiates a new MachineOfMachine.
   *
   * @param string $initialState
   *   The initial state of the machine.
   *
   * @return \Maquina\StateMachine\IStateMachine
   *   A state machine.
   */
    protected function newMachineOfMachine(string $initialState)
    {
        return new MachineOfMachines([$initialState]);
    }

  /**
   * Feeds the feeder.
   *
   * @param string $sql
   *   An sql statement.
   * @param \Maquina\StateMachine\IStateMachine $machine
   *   The machine to feed.
   *
   * @throws \Exception
   *
   * @codeCoverageIgnore
   */
    protected function feedFeeder(string $sql, IStateMachine $machine)
    {
        return Feeder::feed($sql, $machine);
    }
}