├── .gitignore ├── tests ├── invalid_empty.sql ├── invalid_name.sql ├── true.sql ├── BuilderTest.php └── BuilderWorkTest.php ├── src ├── BuilderException.php └── Builder.php ├── .travis.yml ├── .gitattributes ├── phpunit.xml.dist ├── composer.json ├── phpunit └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | bin/* 3 | Vagrantfile 4 | .DS_Store 5 | composer.lock 6 | -------------------------------------------------------------------------------- /tests/invalid_empty.sql: -------------------------------------------------------------------------------- 1 | -- name: select-all 2 | -- request annotation 1 3 | -- request annotation 2 4 | -- name: update 5 | UPDATE REQUEST FROM TABLE 6 | -------------------------------------------------------------------------------- /src/BuilderException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yepsql/yepsql", 3 | "description": "SQL templating helper for PHP inspired by yesql.", 4 | "keywords": ["yesql", "sql", "pdo", "mysql", "postgresql", "sqlite"], 5 | "homepage": "https://github.com/LionHeads/YepSQL", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [{ 9 | "name": "Alexandr Savin", 10 | "email": "lion2head@gmail.com" 11 | }], 12 | "autoload": { 13 | "psr-4": { 14 | "YepSQL\\": "src/" 15 | } 16 | }, 17 | 18 | "require": { 19 | "php": ">=5.4.0", 20 | "ext-pdo": "*" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "4.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/true.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -- name: user-request 5 | -- request invalid 6 | SQL REQUEST; 7 | 8 | 9 | -- name: select-all 10 | -- request annotation 1 11 | -- request annotation 2 12 | SELECT * 13 | FROM table 14 | WHERE user_id = :user_id; 15 | -- request annotation 1511411 16 | 17 | -- name: workTest 18 | SELECT count(*) FROM `work_test`; 19 | 20 | 21 | 22 | -- name: newValue 23 | UPDATE `work_test` SET `value` = :value WHERE `key` = :key; 24 | 25 | 26 | -- name: update 27 | -- request invalid 28 | UPDATE REQUEST FROM TABLE; 29 | 30 | -- name: work-test 31 | SELECT count(*) 32 | FROM `work_test`; 33 | 34 | -- name: work-fail-test 35 | SELECT count(*) 36 | FROM `work_test_incorreasdksald`; 37 | -------------------------------------------------------------------------------- /phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | if (version_compare('5.6.0', PHP_VERSION, '>')) { 13 | fwrite( 14 | STDERR, 15 | 'This version of PHPUnit requires PHP 5.6; using the latest version of PHP is highly recommended.' . PHP_EOL 16 | ); 17 | 18 | die(1); 19 | } 20 | 21 | if (!ini_get('date.timezone')) { 22 | ini_set('date.timezone', 'UTC'); 23 | } 24 | 25 | require __DIR__ . '/vendor/autoload.php'; 26 | 27 | PHPUnit_TextUI_Command::main(); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YepSQL [![Build Status](https://travis-ci.org/LionsHead/YepSQL.svg?branch=master)](https://travis-ci.org/LionsHead/YepSQL) 2 | SQL templating helper for PHP inspired by [yesql](https://github.com/krisajenkins/yesql). 3 | 4 | Requirements: 5 | php >= 5.6; 6 | pdo_extension; 7 | 8 | Install: 9 | composer require yepsql/yepsql 10 | 11 | ## Usage 12 | Create a file containing your SQL queries 13 | ### File example: 14 | ````sql 15 | -- name: sqlQueryName 16 | -- query annotation 17 | SELECT count(*) FROM `table`; 18 | 19 | -- name: getUsersInfo 20 | -- request annotation 1 ... 21 | -- request annotation 2 ... 22 | SELECT * 23 | FROM `table` 24 | WHERE `user_id` = ? ; 25 | 26 | -- name: updateUserName 27 | UPDATE `table` 28 | SET `user_name` = :user_name 29 | WHERE `user_id` = :user_id ; 30 | ```` 31 | 32 | And call them in your code. 33 | Notice: "query-name" is converted to "query_name", php does not support this name methods. 34 | 35 | ### Example: 36 | 37 | ````php 38 | 39 | $sql_template = new \YepSQL\Builder( 40 | new PDO_instance('sqlite::memory:'), // you instance of PDO 41 | '/path/to/file.sql' // file with queries 42 | ); 43 | 44 | 45 | // prepare SELECT * FROM `table` WHERE `user_id` = ? ; 46 | // and send query "getUsersInfo": 47 | // SELECT * FROM `table` WHERE `user_id` = 128; 48 | $user_id = 128; // request arguments 49 | $stmt = $sql_template->getUsersInfo($user_id); 50 | // returned PDOStatement instance 51 | $user_data = $stmt->fetch(PDO::FETCH_ASSOC); 52 | 53 | // send query "updateUserName": 54 | // UPDATE `table` SET `user_name` = 'NewUSerName' WHERE `user_id` = '128'; 55 | $sql_template->updateUserName([ 56 | ':user_name' => 'NewUSerName', 57 | ':user_id' => 128 58 | ]); 59 | 60 | ```` 61 | 62 | Enjoy. 63 | -------------------------------------------------------------------------------- /tests/BuilderTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\YepSQL\Builder', $builder); # 15 | 16 | $this->assertContains('SQL REQUEST', $builder->getQuery('user_request')); # 17 | $this->assertContains('WHERE user_id = :user_id', $builder->getQuery('select_all')); # 18 | $this->assertContains('UPDATE REQUEST FROM TABLE', $builder->getQuery('update')); # 19 | $this->assertContains('FROM `work_test`', $builder->getQuery('work_test')); # 20 | } 21 | 22 | public function testInvalidQueryName() 23 | { 24 | 25 | $file = __DIR__.'/invalid_name.sql'; 26 | $builder = new Builder(new PDO_instance('sqlite::memory:')); 27 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 28 | 29 | /* Exception */ 30 | $this->setExpectedException('Exception'); # 31 | $builder->loadFromFile($file); 32 | 33 | } 34 | 35 | public function testInvalidQueryEmpty() 36 | { 37 | /* Exception */ 38 | $this->setExpectedException('\YepSQL\BuilderException', 'Query "select_all" is empty!'); 39 | 40 | $file = __DIR__.'/invalid_empty.sql'; 41 | $builder = new Builder(new PDO_instance('sqlite::memory:'), $file); 42 | } 43 | 44 | public function testInvalidQueryUnknow() 45 | { 46 | $file = __DIR__.'/true.sql'; 47 | $builder = new Builder(new PDO_instance('sqlite::memory:'), $file); 48 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 49 | 50 | /* Exception */ 51 | $this->setExpectedException('\YepSQL\BuilderException', 'Query "query_unknow" does not exist'); # 52 | $builder->query_unknow(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | PDO::PARAM_BOOL, 15 | 'is_float' => PDO::PARAM_STR, 16 | 'is_int' => PDO::PARAM_INT, 17 | 'is_null' => PDO::PARAM_NULL, 18 | 'is_string' => PDO::PARAM_STR, 19 | ]; 20 | 21 | /** 22 | * @param object $pdo instance of PDO 23 | * @param string $filepath the path to file with queries 24 | */ 25 | function __construct(PDO $pdo, $filepath = null) 26 | { 27 | $this->pdo = $pdo; 28 | if (!is_null($filepath)) { 29 | $this->loadFromFile($filepath); 30 | } 31 | } 32 | 33 | /** 34 | * Query call: $yepsql_instance->query_name($args); 35 | * @param string $name - query name 36 | * @param array $args - query params 37 | */ 38 | public function __call($name, array $args = []) 39 | { 40 | if (!isset($this->queries[$name])) { 41 | throw new BuilderException('Query "'. $name .'" does not exist', 4); 42 | } 43 | 44 | $r = $this->pdo->prepare($this->queries[$name]); 45 | $args = empty($args) ? $args : $args[0]; 46 | 47 | foreach ($args as $argk => $argv) { 48 | foreach($this->type_map as $php_type => $pdo_type) { 49 | if ($php_type($argv)) { 50 | $r->bindValue($argk, $argv, $pdo_type); 51 | break; 52 | } 53 | 54 | /* Try to bind it anyway if we didn't find a match in the type 55 | map */ 56 | $r->bindValue($argk, $argv); 57 | } 58 | } 59 | 60 | $r->execute(); 61 | return $r; 62 | } 63 | 64 | /** 65 | * Creates a query from a file 66 | * @param string $path - path to file 67 | */ 68 | public function loadFromFile($filepath) 69 | { 70 | if (!file_exists($filepath)) { 71 | throw new BuilderException("File not exists", 1); 72 | } 73 | $data = explode("\n", file_get_contents($filepath)); 74 | $tag = []; 75 | foreach ($data as $line => $string) { 76 | // remove extra characters (windows \r) 77 | $string = str_replace("\r", '', $string); 78 | if (preg_match("/^\s*--\s*name:\s*([a-zA-Z0-9_-]+)/", $string, $name)) { 79 | // complete previous query 80 | if (!empty($tag)) $this->createQueryWithTags($tag); 81 | // new query 82 | $tag = [ 83 | 'name' => strtr($name[1], '-', '_'), 84 | 'query' => null 85 | ]; 86 | } elseif (preg_match('/^\s*--/', $string) || empty($string)) { 87 | // sql comment or empty line 88 | // do nothing 89 | } elseif (empty($tag) && !preg_match('/^\s*--/', $string)) { 90 | // invalid file: 91 | throw new BuilderException('Parse error: the query definition without a "name:" line '.$line, 2); 92 | } else { 93 | // new query line 94 | $tag['query'] .= $string."\n"; 95 | } 96 | } 97 | $this->createQueryWithTags($tag); 98 | return $this->queries; 99 | } 100 | 101 | /** 102 | * Create new query 103 | * @param array $tag - request [name, query] 104 | * @return array - new empty array 105 | */ 106 | private function createQueryWithTags(array $tag) 107 | { 108 | if (empty($tag['query'])) { 109 | throw new BuilderException('Query "'.$tag['name'].'" is empty!', 3); 110 | } 111 | $this->queries[$tag['name']] = $tag['query']; 112 | } 113 | 114 | /** 115 | * return query body 116 | * @param string $name - request name 117 | * @return string $query - request code 118 | */ 119 | public function getQuery($name) 120 | { 121 | return isset($this->queries[$name]) ? $this->queries[$name] : false; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/BuilderWorkTest.php: -------------------------------------------------------------------------------- 1 | \PDO::ERRMODE_EXCEPTION]); 14 | $pdo->query('CREATE TABLE IF NOT EXISTS `work_test` ( 15 | `key` varchar(128) DEFAULT NULL, 16 | `value` varchar(256) DEFAULT NULL 17 | );'); 18 | $pdo->query(" DELETE FROM `work_test`;"); // clear table data 19 | 20 | $pdo->query("INSERT INTO `work_test` (`key`, `value`) VALUES ('key1', 'val1');"); 21 | $pdo->query("INSERT INTO `work_test` (`key`, `value`) VALUES ('key2', 'val2');"); 22 | $pdo->query("INSERT INTO `work_test` (`key`, `value`) VALUES ('key3', 'val3');"); 23 | 24 | $count = $pdo->query('SELECT count(*) FROM `work_test`;')->fetchColumn(); 25 | $builder = new Builder($pdo); 26 | 27 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 28 | 29 | $queries = $builder->loadFromFile(__DIR__.'/true.sql'); 30 | 31 | $this->assertEquals(7, count($queries)); # 32 | $this->assertEquals($count, $builder->workTest()->fetchColumn()); # 33 | } 34 | 35 | public function testWorkFail() 36 | { 37 | // work_test 38 | $pdo = new PDO_instance('sqlite::memory:', null, null, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); 39 | $builder = new Builder($pdo); 40 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 41 | 42 | $builder->loadFromFile(__DIR__.'/true.sql'); 43 | /* Exception */ 44 | $this->setExpectedException('PDOException'); # 45 | 46 | $builder->work_fail_test(); 47 | } 48 | 49 | public function testUpdate() 50 | { 51 | // work_test 52 | $pdo = new PDO_instance('sqlite::memory:', null, null, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); 53 | $pdo->query('CREATE TABLE IF NOT EXISTS `work_test` ( 54 | `key` varchar(128) DEFAULT NULL, 55 | `value` varchar(256) DEFAULT NULL 56 | );'); 57 | $pdo->query(" DELETE FROM `work_test`;"); // clear table data 58 | 59 | $pdo->query("INSERT INTO `work_test` (`key`, `value`) VALUES ('keyTest', 'val1');"); 60 | 61 | $builder = new Builder($pdo, __DIR__.'/true.sql'); 62 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 63 | 64 | $builder->newValue([ 65 | ':key' => 'keyTest', 66 | ':value' => 'new_value' 67 | ]); 68 | 69 | $result = $pdo->query("SELECT `key`, `value` FROM `work_test` WHERE `key` = 'keyTest';")->fetch(); 70 | 71 | $this->assertEquals('keyTest', $result['key']); # 72 | $this->assertEquals('new_value', $result['value']); # 73 | } 74 | 75 | public function testLongWork() 76 | { 77 | // work_test 78 | $pdo = new PDO_instance('sqlite::memory:', null, null, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); 79 | $pdo->query('CREATE TABLE IF NOT EXISTS `work_test` ( 80 | `key` varchar(128) DEFAULT NULL, 81 | `value` varchar(256) DEFAULT NULL 82 | );'); 83 | $pdo->query(" DELETE FROM `work_test`;"); // clear table data 84 | 85 | for ($i = 1; $i <= 100; $i++) { 86 | $pdo->query("INSERT INTO `work_test` (`key`, `value`) VALUES ('key". $i ."', '". mt_rand(0, 50) ."');"); 87 | } 88 | 89 | $builder = new Builder($pdo); 90 | 91 | $this->assertInstanceOf('\YepSQL\Builder', $builder); # 92 | 93 | $this->assertEquals(3230, count( $builder->loadFromFile(__DIR__.'/true_long.sql') )); 94 | 95 | $count = $pdo->query('SELECT count(*) FROM `work_test` WHERE `value` > 32;')->fetchColumn(); 96 | 97 | $re = $builder->workLongMore([':more' => 32]) 98 | ->fetchColumn(); 99 | print_r($re); 100 | $this->assertEquals($count, $re); # 101 | 102 | $count = $pdo->query('SELECT count(*) FROM `work_test` WHERE `value` < 32;')->fetchColumn(); 103 | $re = $builder->workLongLess([':less' => 32])->fetchColumn(); 104 | print_r($re); 105 | $this->assertEquals($count, $re); # 106 | 107 | } 108 | } 109 | --------------------------------------------------------------------------------