php-yaoi/php-yaoi

View on GitHub
src/Database/Mysql/CreateTableReader.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

namespace Yaoi\Database\Mysql;

use Yaoi\BaseClass;
use Yaoi\Database;
use Yaoi\Database\Definition\Column;
use Yaoi\Database\Definition\Index;
use Yaoi\Database\Definition\Table;
use Yaoi\Sql\Raw;
use Yaoi\String\Lexer;
use Yaoi\String\Parser;

class CreateTableReader extends BaseClass
{
    private $statement;
    private $database;
    public function __construct($statement, Database\Contract $database) {
        $this->database = $database;
        $this->statement = $statement;
    }

    /** @var Lexer\Token[]  */
    private $binds = array();
    private $tableName;
    /** @var  \stdClass */
    private $columns;
    private $indexes = array();
    private $foreignKeys = array();
    /** @var  Table */
    private $table;


    const BIND_PREFIX = ':B';
    const BIND_POSTFIX = 'B:';

    /**
     * @param $index
     * @return string|Lexer\Token|Lexer\Parsed
     */
    private function resolve($index) {
        if (strpos($index, self::BIND_PREFIX) !== false) {
            $index = (string)Parser::create($index)->inner(self::BIND_PREFIX, self::BIND_POSTFIX);
        }

        $key = self::BIND_PREFIX . $index . self::BIND_POSTFIX;
        if (!isset($this->binds[$key])) {
            return $index;
        }

        $bind = $this->binds[$key];
        if ($bind instanceof Lexer\Token) {
            return $bind->unEscapedContent;
        }
        elseif ($bind instanceof Lexer\Parsed) {
            return $bind;
        }
    }

    /** @var  Lexer\Parsed[] */
    private $lines;
    private function tokenize() {
        $tokenizer = Utility::create()->getStatementTokenizer();

        $tokens = $tokenizer->tokenize($this->statement);
        $renderer = new Lexer\Renderer();
        $renderer
            ->setBindKey(self::BIND_PREFIX, self::BIND_POSTFIX)
            ->strip('#', ' --')
            ->keepBoundaries('(');

        $expression = $renderer->getExpression($tokens);
        $this->binds = $expression->getBinds();

        $statement = new Parser($expression->getStatement());
        $tableName = trim($statement->inner('CREATE TABLE', '(', false, true));

        $this->tableName = $this->resolve($tableName);
        /** @var Lexer\Parsed $lines */
        $bind = $statement->inner(null, ')');
        $lines = $this->resolve($bind);
        $this->lines = $lines->split(',');
    }

    protected function isAutoId(Parser $column) {
        return $column->contain('AUTO_INCREMENT', true);
    }

    private function parseColumn(Parser $parser) {
        $columnName = $parser->inner(null, array(' ',"\r", "\n", "\t"));
        $columnName = (string)$this->resolve(trim($columnName));

        $type = (string)$parser->inner(null, ' ');
        $unsigned = $parser->contain('UNSIGNED', true);
        $notNull = $parser->contain('NOT NULL', true);
        $autoId = $this->isAutoId($parser);
        $default = $parser->inner('DEFAULT ', null, false, true);
        if (!$default->isEmpty()) {
            $default = (string)$default;
            $defUpper = strtoupper($default);
            if ('NULL' === $defUpper) {
                $default = null;
            } elseif ('CURRENT_TIMESTAMP' === $defUpper) {
                $default = new Raw($defUpper);
            } elseif (strpos($default, self::BIND_PREFIX) !== false) {
                $default = $this->resolve($default);
            }
        }
        else {
            $default = false;
        }

        $flags = $this->getTypeByString($type);

        if ($notNull) {
            $flags += Column::NOT_NULL;
        }

        if ($autoId) {
            $flags += Column::AUTO_ID;
        }

        if ($unsigned) {
            $flags += Column::UNSIGNED;
        }

        $column = new Column($flags);

        if ($length = (string)$parser->setOffset(0)->inner('VARCHAR(', ')', false, true)) {
            $column->setStringLength($length, false);
        }
        elseif ($length = (string)$parser->setOffset(0)->inner('CHAR(', ')', false, true)) {
            $column->setStringLength($length, true);
        }


        $column->setDefault($default);

        $column->schemaName = $columnName;
        $this->columns->$columnName = $column;
    }

    private function parseLines() {
        $this->columns = new \stdClass();
        $this->indexes = array();
        $this->foreignKeys = array();

        $renderer = new Lexer\Renderer();
        $renderer
            ->keep('(')
            ->strip('-- ', '#')
            ->setBindKey(self::BIND_PREFIX, self::BIND_POSTFIX);


        foreach ($this->lines as $line) {
            $expression = $renderer->getExpression($line);
            $statement = $expression->getStatement();

            $this->binds = $expression->getBinds();

            $statement = trim($statement);
            $parser = new Parser($statement);

            if ($parser->starts('PRIMARY KEY', true)) {
                $indexColumns = $parser->inner('(', ')')->explode(',');
                foreach ($indexColumns as &$columnName) {
                    $columnName = $this->resolve(trim($columnName));
                }
                $this->indexes []= array(Index::TYPE_PRIMARY, Index::TYPE_PRIMARY, $indexColumns);
            }
            elseif ($parser->starts('UNIQUE KEY', true)) {
                $indexName = $this->resolve(trim($parser->inner('KEY', '(')));
                $indexColumns = $parser->inner(null, ')')->explode(',');

                foreach ($indexColumns as &$columnName) {
                    $columnName = $this->resolve($columnName);
                }
                $this->indexes []= array(Index::TYPE_UNIQUE, $indexName, $indexColumns);
            }
            elseif ($parser->starts('KEY', true)) {
                $indexName = $this->resolve(trim($parser->inner('KEY', '(', false, true)));
                $indexColumns = $parser->inner(null, ')')->explode(',');
                foreach ($indexColumns as &$columnName) {
                    $columnName = $this->resolve($columnName);
                }
                $this->indexes []= array(Index::TYPE_KEY, $indexName, $indexColumns);
            }
            elseif ($parser->starts('CONSTRAINT', true)) {
                $this->parseConstraint($parser);
            }
            else {
                $this->parseColumn($parser);
            }

        }
    }


    private function parseConstraint(Parser $parser) {
        $indexName = $this->resolve($parser->inner('CONSTRAINT', 'FOREIGN KEY', false, true));
        $indexColumns = $parser->inner('(', ')')->explode(',');
        foreach ($indexColumns as &$columnName) {
            $columnName = $this->resolve($columnName);
        }

        $referenceName = $this->resolve($parser->inner('REFERENCES', '(', false, true));
        $referenceColumns = $parser->inner(null, ')')->explode(',');
        foreach ($referenceColumns as &$columnName) {
            $columnName = $this->resolve($columnName);
        }

        $ons = $parser->inner()->explode('ON ');
        $onUpdate = $onDelete = null;
        if (count($ons) > 1) {
            unset($ons[0]);
            foreach ($ons as $on) {
                $on = trim(strtoupper($on));
                $on = explode(' ', $on, 2);
                if ($on[0] === 'UPDATE') {
                    $onUpdate = trim($on[1]);
                }
                elseif ($on[0] === 'DELETE') {
                    $onDelete = trim($on[1]);
                }
            }
        }
        $this->foreignKeys []= array($indexName, $indexColumns, $referenceName, $referenceColumns, $onUpdate, $onDelete);
    }

    private function buildIndexes() {
        foreach ($this->indexes as $indexData) {
            $type = $indexData[0];
            $name = $indexData[1];

            $columns = array();
            foreach ($indexData[2] as $columnName) {
                $columns []= $this->columns->$columnName;
            }
            $index = new Index($columns);
            $index->setName($name);
            $index->setType($type);
            $this->table->addIndex($index);
        }
    }

    private function buildForeignKeys() {
        foreach ($this->foreignKeys as $data) {
            $name = $data[0];
            $localColumnNames = $data[1];
            $localColumns = array();
            foreach ($localColumnNames as &$columnName) {
                $localColumns []= $this->columns->$columnName;
            }


            $referenceTableName = $data[2];
            $referenceColumnNames = $data[3];

            $referenceColumns = array();
            foreach ($referenceColumnNames as &$columnName) {
                $column = new Column();
                $column->schemaName = $columnName;
                $column->table = new Table(null, null, $referenceTableName);

                $referenceColumns []= $column;
            }

            $foreignKey = new Database\Definition\ForeignKey($localColumns, $referenceColumns);

            // ON UPDATE
            if ($data[4]) {
                $foreignKey->onUpdate = $data[4];
            }

            // ON DELETE
            if ($data[5]) {
                $foreignKey->onDelete = $data[5];
            }

            $foreignKey->setName($name);
            $this->table->addForeignKey($foreignKey);
        }
    }

    public function getDefinition() {
        $this->tokenize();
        $this->parseLines();

        $this->table = new Table($this->columns, $this->database, $this->tableName);

        $this->buildIndexes();
        $this->buildForeignKeys();

        return $this->table;
    }


    public function getTypeByString($type) {
        $phpType = Column::STRING;
        $type = strtoupper($type);
        switch (true) {
            case 'BIGINT' === substr($type, 0, 6):
                $phpType = Column::INTEGER + Column::SIZE_8B;
                break;
            case 'INT' === substr($type, 0, 3):
                $phpType = Column::INTEGER;
                break;
            case 'MEDIUMINT' === substr($type, 0, 9):
                $phpType = Column::INTEGER + Column::SIZE_3B;
                break;
            case 'SMALLINT' === substr($type, 0, 8):
                $phpType = Column::INTEGER + Column::SIZE_2B;
                break;
            case 'TINYINT' === substr($type, 0, 7):
                $phpType = Column::INTEGER + Column::SIZE_1B;
                break;

            case 'DECIMAL' === substr($type, 0, 7):
                $phpType = Column::FLOAT;
                break;
            case 'DOUBLE' === $type:
                $phpType = Column::FLOAT + Column::SIZE_8B;
                break;
            case 'FLOAT' === $type:
                $phpType = Column::FLOAT;
                break;

            case 'DATE' === $type:
            case 'DATETIME' === $type:
            case 'TIMESTAMP' === $type:
                $phpType = Column::TIMESTAMP;
                break;

        }
        return $phpType;
    }





}