"doctrine/dbal": "v2.5.13",
"d11wtq/boris": "dev-master",
"andy128k/missing-tools": "0.2.*@dev",
- "andy128k/migrations": "dev-master",
"andy128k/pegp": "0.2",
- "andy128k/routeexpression": "dev-master",
"pimple/pimple": "~3.0",
"twig/twig": "~1"
},
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "60b3f404429126af3c0dcaee85fcc316",
+ "content-hash": "73a01ff77b33bdec0cab728b7116b494",
"packages": [
- {
- "name": "andy128k/migrations",
- "version": "dev-master",
- "source": {
- "type": "git",
- "url": "git://git.andy128k.net/migrations.git",
- "reference": "3d5a19ba8586db7e4405b3359c6918576a5260be"
- },
- "require": {
- "php": ">=5.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Andrey Kutejko",
- "email": "andy128k@gmail.com"
- }
- ],
- "description": "Simple DB schema migrations",
- "time": "2015-03-30T17:22:28+00:00"
- },
{
"name": "andy128k/missing-tools",
"version": "0.2.x-dev",
"description": "Library to construct PEG parsers",
"time": "2019-03-17T13:12:33+00:00"
},
- {
- "name": "andy128k/routeexpression",
- "version": "dev-master",
- "source": {
- "type": "git",
- "url": "git://git.andy128k.net/routeexpression.git",
- "reference": "0c5d0edce61a68fa27bc9b13278a06745ecdc5fe"
- },
- "require-dev": {
- "phpunit/phpunit": "4.4.*"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src"
- ]
- },
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Andrey Kutejko",
- "email": "andy128k@gmail.com"
- }
- ],
- "description": "Route expression",
- "time": "2015-01-11T10:36:51+00:00"
- },
{
"name": "d11wtq/boris",
"version": "dev-master",
"minimum-stability": "dev",
"stability-flags": {
"d11wtq/boris": 20,
- "andy128k/missing-tools": 20,
- "andy128k/migrations": 20,
- "andy128k/routeexpression": 20
+ "andy128k/missing-tools": 20
},
"prefer-stable": false,
"prefer-lowest": false,
--- /dev/null
+* Route Expression
+
+** Build expression
+
+ There are two ways to build expression: using fluent api and compiling.
+
+*** Fluent API
+
+ Create instance of RouteExpression class and compose expression by chaining parts.
+
+ Example: ~$expr = (new RouteExpression)->optional('/ru')->just('/story/');~
+
+ Available parts:
+
+ 1. ~just($text)~ -- literal text;
+ 2. ~oneOf($choice1, $choice2, ...)~ -- one of given choices;
+ 3. ~oneOfv($choices)~ -- same as above, except it takes array of choices;
+ 4. ~optional($option)~ -- optional text, same as ~oneOf($option, '')~;
+ 5. ~id()~ -- nonempty sequence of digits;
+ 6. ~slug()~ -- nonempty sequence of letters, digits, underscores and dashes.
+
+*** Compilation
+
+ Route expression can be compiled from text representation.
+
+ Example: ~$expr = RouteExpression::compile('[/ru]/story/');~
+
+ Text enclosed in brackets ~[]~ is treated as ~optional~.
+ Text enclosed in braces ~{}~ is treated as ~oneOf~. Choices are separated by ~|~ pipe character.
+ Id and slug are written as ~:id~ and ~:slug~.
+
+** Matching
+
+ #+BEGIN_SRC php
+ bool match($url, &$matches)
+ #+END_SRC
+
+ This method returns ~true~ on successfull match of ~$url~ and matched parts in ~$matches~ array.
+
+ Examples.
+
+ ~RouteExpression::compile('[/ru]/story/')->match('/story/', $matches)~ returns ~true~ and ~$matches~ contains empty string which corresponds to optional part of expression.
+
+ ~RouteExpression::compile('/article/:slug')->match('/article/php-is-awesome', $matches)~ returns ~true~ and ~$matches~ contains only string 'php-is-awesome'.
+
+** Reversing
+
+ #+BEGIN_SRC php
+ string reverse($matches)
+ #+END_SRC
+
+ This method substitutes values from ~$matches~ array to the expression and returns url.
+
+ Example.
+
+ ~RouteExpression::compile('/article/:slug')->reverse(array('/article/php-is-awesome')~ returns '/article/php-is-awesome'.
+
+++ /dev/null
-* Template
-
- This document describes template language of IPF framework.
-
- Templates are placed in 'templates' directories of project or application.
-
- =[ ]?= -- optional code
-
- =[ ]*= -- code can be repeated zero or more times.
-
-** Tags
-
-*** url
- Syntax: ={url '<view-name>'[, array([<arg>]*)]?}=
-
- Examples: ={url 'Content_View_Index'}=, ={url 'Catalog_View_Products', array($product->slug)}=, ={url 'News_View_Item', array($item->date, $item->slug)}=
-
- Reverse url for a given view and its parameters.
-
-*** sql
- Syntax: ={sql}=
-
- Prints SQL statistics.
-
-** Configuration
-
- + =template_tags= -- additional template tags
- + =template_modifiers= -- additional template modifiers
- + =template_context_processors= -- list of context processors
- + =debug= -- forces to recompile template before each rendering
-
<?php
-class Migration_20140103000000 extends \PFF\Migrations\Migration
+class Migration_20140103000000
{
- function migrate()
+ function migrate(\Doctrine\DBAL\Connection $connection)
{
- $this->connection->exec('CREATE TABLE IF NOT EXISTS admin_log (' .
+ $connection->exec('CREATE TABLE IF NOT EXISTS admin_log (' .
'id BIGINT AUTO_INCREMENT,' .
'username VARCHAR(32),' .
'user_id BIGINT,' .
'DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB');
}
}
-
<?php
-class Migration_20140102000000 extends \PFF\Migrations\Migration
+class Migration_20140102000000
{
- function migrate()
+ function migrate(\Doctrine\DBAL\Connection $connection)
{
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_user ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_user ('.
'id BIGINT AUTO_INCREMENT,'.
'username VARCHAR(32) NOT NULL UNIQUE,'.
'password VARCHAR(128) NOT NULL,'.
'PRIMARY KEY(id)'.
') DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB');
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_role ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_role ('.
'id BIGINT AUTO_INCREMENT,'.
'name VARCHAR(255) UNIQUE,'.
'PRIMARY KEY(id)'.
') DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB');
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_permission ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_permission ('.
'id BIGINT AUTO_INCREMENT,'.
'name VARCHAR(255) NOT NULL UNIQUE,'.
'PRIMARY KEY(id)'.
') DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB');
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_user_permission ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_user_permission ('.
'user_id BIGINT,'.
'permission_id BIGINT,'.
'FOREIGN KEY(user_id) REFERENCES auth_user(id) ON DELETE CASCADE,'.
'PRIMARY KEY(user_id, permission_id)'.
') ENGINE = INNODB');
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_user_role ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_user_role ('.
'user_id BIGINT,'.
'role_id BIGINT,'.
'FOREIGN KEY(user_id) REFERENCES auth_user(id) ON DELETE CASCADE,'.
'PRIMARY KEY(user_id, role_id)'.
') ENGINE = INNODB');
- $this->connection->exec('CREATE TABLE IF NOT EXISTS auth_role_permission ('.
+ $connection->exec('CREATE TABLE IF NOT EXISTS auth_role_permission ('.
'role_id BIGINT,'.
'permission_id BIGINT,'.
'FOREIGN KEY(role_id) REFERENCES auth_role(id) ON DELETE CASCADE,'.
') ENGINE = INNODB');
}
}
-
<?php
+use IPF\Migrations\Migrations;
+
class IPF_Command_CreateMigration
{
public $command = 'createmigration';
public function run($args=null)
{
- $m = new \PFF\Migrations\Migrations;
+ $m = new Migrations;
$filename = $m->createMigration(IPF::get('project_root').'/project/db/migrations', implode('_', $args));
echo "Create new migration $filename\n";
}
}
-
<?php
use Doctrine\DBAL\Connection;
+use IPF\Migrations\Migrations;
class IPF_Command_Migrate
{
$paths[] = $app->getPath() . 'migrations';
}
- $m = new \PFF\Migrations\Migrations;
+ $m = new Migrations;
$m->migrate($this->connection, $paths);
}
}
--- /dev/null
+<?php
+
+namespace IPF\Migrations;
+
+use Doctrine\DBAL\Connection;
+
+class Migrations
+{
+ public function migrate(Connection $connection, array $paths)
+ {
+ $this->ensureLogExists($connection);
+
+ $files = array();
+ foreach ($paths as $path)
+ $files = array_merge($files, glob($path.'/*.php'));
+
+ usort($files, function($a, $b) {
+ return strcmp(
+ substr(basename($a), 0, 14),
+ substr(basename($b), 0, 14));
+ });
+ foreach ($files as $file) {
+ $ts = substr(basename($file), 0, 14);
+
+ if ($this->migrationPassed($connection, $ts))
+ continue;
+
+ echo "Run migration $file\n";
+ require_once $file;
+
+ $connection->beginTransaction();
+ try {
+ $migration = 'Migration_'.$ts;
+ $migration = new $migration;
+ $migration->migrate($connection);
+
+ $this->markMigration($connection, $ts);
+ } catch (\Exception $e) {
+ $connection->rollback();
+ throw $e;
+ }
+ $connection->commit();
+ }
+ }
+
+ public function createMigration($path, $hint='')
+ {
+ $ts = date('YmdHis');
+
+ $name = preg_replace('/[^a-z0-9]+/i', '_', $hint);
+ $name = preg_replace('/__+/', '_', $name);
+ $name = preg_replace('/^_/i', '', $name);
+ $name = preg_replace('/_$/i', '', $name);
+ if ($name)
+ $name = '_'.$name;
+
+ if (!is_dir($path))
+ mkdir($path, 0777, true);
+
+ $filename = $path.'/'.$ts.$name.'.php';
+
+ $template = file_get_contents(dirname(__FILE__) . '/template');
+ $content = str_replace('%TS%', $ts, $template);
+ file_put_contents($filename, $content);
+
+ return $filename;
+ }
+
+ private function ensureLogExists(Connection $connection)
+ {
+ $st = $connection->query("SHOW TABLES LIKE 'migration_log'");
+ $exists = $st->fetch();
+ $st->closeCursor();
+
+ if (!$exists) {
+ $connection->exec('CREATE TABLE migration_log (ts CHAR(14) NOT NULL PRIMARY KEY) ENGINE = INNODB');
+ }
+ }
+
+ private function migrationPassed(Connection $connection, $ts)
+ {
+ return (int)$connection->fetchColumn("SELECT COUNT(1) FROM migration_log WHERE ts = ?", [$ts]) > 0;
+ }
+
+ private function markMigration(Connection $connection, $ts)
+ {
+ $connection->insert('migration_log', ['ts' => $ts]);
+ }
+}
--- /dev/null
+<?php
+
+class Migration_%TS%
+{
+ function migrate(\Doctrine\DBAL\Connection $connection)
+ {
+ // $data = $connection->fetchAll('SELECT * FROM table WHERE param = ?', ['value']);
+ // $connection->exec('CREATE TABLE table (id BIGINT AUTO_INCREMENT, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB');
+ }
+}
<?php
+use IPF\Router\Expression;
+
class IPF_Router
{
/** @var IPF_Route[] */
$p = $prefix . \PFF\Arr::pop($route, 'prefix', '');
$this->flattenRoutes($nested, $p, $route);
} elseif (isset($route['regex'])) {
- $this->routes[] = new IPF_Route($prefix . $route['regex'], 'IPF_Router_RegexMatch', $route);
+ $this->routes[] = new IPF_Route($prefix . $route['regex'], IPF_Router_RegexMatch::class, $route);
} elseif (isset($route['expr'])) {
- $this->routes[] = new IPF_Route($prefix . $route['expr'], 'RouteExpression', $route);
+ $this->routes[] = new IPF_Route($prefix . $route['expr'], Expression::class, $route);
} else {
throw new IPF_Exception('Unsupported route type');
}
--- /dev/null
+<?php
+
+namespace IPF\Router;
+
+final class Expression
+{
+ private $expression, $regex;
+
+ public function __construct()
+ {
+ $this->expression = array();
+ $this->regex = '';
+ }
+
+ public function just($text)
+ {
+ if ($text) {
+ $this->expression[] = array('text', $text);
+ $this->regex .= preg_quote($text, '#');
+ }
+ return $this;
+ }
+
+ public function oneOf()
+ {
+ return $this->oneOfv(func_get_args());
+ }
+
+ public function oneOfv($options)
+ {
+ $quoted = array();
+ foreach ($options as $option) {
+ $quoted[] = preg_quote($option, '#');
+ }
+ $this->regex .= '(' . implode('|', $quoted) . ')';
+
+ array_unshift($options, 'oneOf');
+ $this->expression[] = $options;
+ return $this;
+ }
+
+ public function optional($option)
+ {
+ $this->expression[] = array('optional', $option);
+ $this->regex .= '((?:' . preg_quote($option, '#') . ')?)';
+ return $this;
+ }
+
+ public function id()
+ {
+ $this->expression[] = array('id');
+ $this->regex .= '(\d+)';
+ return $this;
+ }
+
+ public function slug()
+ {
+ $this->expression[] = array('slug');
+ $this->regex .= '([\w\_\-]+)';
+ return $this;
+ }
+
+ public function match($url, &$matches)
+ {
+ if (1 == preg_match('#^'.$this->regex.'$#', $url, $matches)) {
+ array_shift($matches);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public function reverse($matches)
+ {
+ $matchesIndex = 0;
+ $url = '';
+ foreach ($this->expression as $part) {
+ switch ($part[0]) {
+ case 'text':
+ $url .= $part[1];
+ break;
+ case 'oneOf':
+ $m = $matches[$matchesIndex++];
+ $url .= $m;
+ break;
+ case 'optional':
+ $m = $matches[$matchesIndex++];
+ if ($m && $m !== $part[1]) {
+ throw new \Exception('Wrong optional parameter "'.$m.'". "'.$part[1].'" is expected.');
+ }
+ $url .= $m;
+ break;
+ case 'id':
+ $m = $matches[$matchesIndex++];
+ $url .= $m;
+ break;
+ case 'slug':
+ $m = $matches[$matchesIndex++];
+ $url .= $m;
+ break;
+ }
+ }
+ if ($matchesIndex !== count($matches)) {
+ throw new \Exception('Parameters doesn\'t match to url expression.');
+ }
+ return $url;
+ }
+
+ public function __toString()
+ {
+ $str = '';
+ foreach ($this->expression as $part) {
+ switch ($part[0]) {
+ case 'text':
+ $str .= $part[1];
+ break;
+ case 'oneOf':
+ $str .= '{' . implode('|', array_slice($part, 1)) . '}';
+ break;
+ case 'optional':
+ $str .= '[' . $part[1] . ']';
+ break;
+ case 'id':
+ $str .= ':id';
+ break;
+ case 'slug':
+ $str .= ':slug';
+ break;
+ }
+ }
+ return $str;
+ }
+
+ public static function compile($expression)
+ {
+ $state = 0;
+ $text = '';
+ $result = new self();
+ for ($i = 0; $i < strlen($expression); ++$i) {
+ $ch = $expression[$i];
+ switch ($state) {
+ case 0:
+ switch ($ch) {
+ case '{':
+ $result->just($text);
+ $state = 1;
+ $text = '';
+ break;
+ case '[':
+ $result->just($text);
+ $state = 2;
+ $text = '';
+ break;
+ case ':':
+ $result->just($text);
+ $state = 3;
+ $text = '';
+ break;
+ default:
+ $text .= $ch;
+ break;
+ }
+ break;
+ case 1:
+ if ($ch === '}') {
+ $result->oneOfv(explode('|', $text));
+ $state = 0;
+ $text = '';
+ } else {
+ $text .= $ch;
+ }
+ break;
+ case 2:
+ if ($ch === ']') {
+ $result->optional($text);
+ $state = 0;
+ $text = '';
+ } else {
+ $text .= $ch;
+ }
+ break;
+ case 3:
+ if (ctype_alpha($ch)) {
+ $text .= $ch;
+ } else {
+ if ($text === 'id') {
+ $result->id();
+ } elseif ($text === 'slug') {
+ $result->slug();
+ } else {
+ throw new \Exception('Unsupported route part "'.$text.'".');
+ }
+ $state = 0;
+ $text = $ch;
+ }
+ break;
+ }
+ }
+ if ($state !== 0)
+ throw new \Exception('Unexpected end of route expression.');
+ $result->just($text);
+ return $result;
+ }
+}
<?php
-class Migration_20140101000000 extends \PFF\Migrations\Migration
+class Migration_20140101000000
{
- function migrate()
+ function migrate(\Doctrine\DBAL\Connection $connection)
{
- $this->connection->exec('CREATE TABLE IF NOT EXISTS session (session_key VARCHAR(40), data TEXT, updated_at TIMESTAMP, PRIMARY KEY(session_key)) ENGINE = INNODB');
+ $connection->exec('CREATE TABLE IF NOT EXISTS session (session_key VARCHAR(40), data TEXT, updated_at TIMESTAMP, PRIMARY KEY(session_key)) ENGINE = INNODB');
}
}
-
--- /dev/null
+<?php
+
+use IPF\Router\Expression;
+
+class RouteExpressionTest extends PHPUnit_Framework_TestCase
+{
+ private function assertMatch(Expression $expression, $url, $params=null)
+ {
+ $m = array();
+ $r = $expression->match($url, $m);
+ $this->assertTrue($r);
+ if ($params !== null)
+ $this->assertEquals($m, $params);
+ }
+
+ private function assertNotMatch(Expression $expression, $url)
+ {
+ $m = array();
+ $r = $expression->match($url, $m);
+ $this->assertFalse($r);
+ }
+
+ public function testOptional()
+ {
+ $re = new Expression;
+ $e = $re->optional('/ru')->just('/story/');
+ $this->assertNotMatch($e, '/story');
+ $this->assertMatch($e, '/story/', array(''));
+ $this->assertNotMatch($e, '/en/story/');
+ $this->assertMatch($e, '/ru/story/', array('/ru'));
+ $this->assertNotMatch($e, '/ru/story');
+ }
+
+ public function testOptionalAndSlug()
+ {
+ $re = new Expression;
+ $e = $re->just('/')->optional('ru/')->just('story/')->slug()->just('/');
+ $this->assertNotMatch($e, '/story/wrghsh');
+ $this->assertMatch($e, '/story/srghwrghew/', array('', 'srghwrghew'));
+ $this->assertNotMatch($e, '/en/story/wgwrgewrghwrgh/');
+ $this->assertMatch($e, '/ru/story/wrhwrhr4j4hee/', array('ru/', 'wrhwrhr4j4hee'));
+ $this->assertNotMatch($e, '/ru/story/rewewrhewrhe');
+ }
+
+ public function testOptionalAndId()
+ {
+ $re = new Expression;
+ $e = $re->just('/')->optional('ru/')->just('story/')->id()->just('/');
+ $this->assertNotMatch($e, '/story/23');
+ $this->assertMatch($e, '/story/354/', array('', 354));
+ $this->assertMatch($e, '/ru/story/44/', array('ru/', 44));
+ $this->assertNotMatch($e, '/ru/story/-34/');
+ }
+
+ public function testOneOf()
+ {
+ $re = new Expression;
+ $e = $re->oneOf('/ru', '/en')->just('/story/');
+ $this->assertNotMatch($e, '/story/');
+ $this->assertMatch($e, '/ru/story/', array('/ru'));
+ $this->assertMatch($e, '/en/story/', array('/en'));
+ $this->assertNotMatch($e, '/fr/story/');
+ }
+
+ public function testOneOfWithEmptyCase()
+ {
+ $re = new Expression;
+ $e = $re->oneOf('/ru', '/en', '')->just('/story/');
+ $this->assertMatch($e, '/story/', array(''));
+ $this->assertMatch($e, '/ru/story/', array('/ru'));
+ $this->assertMatch($e, '/en/story/', array('/en'));
+ $this->assertNotMatch($e, '/fr/story/');
+ }
+
+ public function testCompiler()
+ {
+ $this->assertInstanceOf(Expression::class, Expression::compile('[/ru]/story/'));
+ $this->assertInstanceOf(Expression::class, Expression::compile('{/ru|/en}/story/'));
+ $this->assertInstanceOf(Expression::class, Expression::compile('{/ru|/en}/story/:slug/'));
+ $this->assertInstanceOf(Expression::class, Expression::compile('{/ru|/en}/story/:id/'));
+ }
+
+ public function testReverseOptional()
+ {
+ $e = Expression::compile('[/ru]/story/');
+ $this->assertEquals('/ru/story/', $e->reverse(array('/ru')));
+ $this->assertEquals('/story/', $e->reverse(array('')));
+ }
+
+ public function testReverseOneOfAndId()
+ {
+ $e = Expression::compile('{/ru|/en}/story/:id/');
+ $this->assertEquals('/ru/story/46536/', $e->reverse(array('/ru', 46536)));
+ $this->assertEquals('/en/story/15/', $e->reverse(array('/en', 0xF)));
+ }
+}