├── .gitignore ├── src ├── Exception.php ├── Literal.php ├── Structure.php ├── Queries │ ├── Json.php │ ├── Delete.php │ ├── Update.php │ ├── Select.php │ ├── Insert.php │ ├── Common.php │ └── Base.php ├── Utilities.php ├── Regex.php └── Query.php ├── phpunit.xml ├── tests ├── _resources │ ├── init.php │ └── fluentdb.sql ├── StructureTest.php ├── Queries │ ├── DeleteTest.php │ ├── InsertTest.php │ ├── UpdateTest.php │ ├── SelectTest.php │ └── CommonTest.php ├── UtilitiesTest.php └── RegexTest.php ├── .travis.yml ├── composer.json ├── readme.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/_resources/init.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); 12 | $pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.2 4 | - 7.3 5 | - 7.4 6 | 7 | services: 8 | - mysql 9 | 10 | env: 11 | - DB_USER=root 12 | 13 | script: phpunit --configuration phpunit.xml 14 | before_script: 15 | - composer install 16 | - mysql -u $DB_USER < tests/_resources/fluentdb.sql 17 | 18 | notifications: 19 | slack: 20 | secure: cyZnqOCV/gO9p23Z8Lr0e4sc3TqXi0v+VQ8neeRTNalYuiwgn9Co1NakCBO7yyku6qyWE9EOaypYBJlZgaLExLAyCGmaSTRduLlE7P1bdcNnkmns0ikoenFzXd5Uq26ExsegGzUGSbjwtzVhiHLUwigPsJNpnwsMOa2Co5ieo04= 21 | 22 | os: linux 23 | group: stable 24 | dist: bionic 25 | -------------------------------------------------------------------------------- /src/Literal.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | /** 25 | * Get literal value 26 | * 27 | * @return string 28 | */ 29 | function __toString() 30 | { 31 | return $this->value; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "envms/fluentpdo", 3 | "description": "FluentPDO is a quick and light PHP library for rapid query building. It features a smart join builder, which automatically creates table joins.", 4 | "keywords": ["db", "database", "dbal", "pdo", "fluent", "query", "builder", "mysql", "oracle"], 5 | "homepage": "https://github.com/envms/fluentpdo", 6 | "license": ["Apache-2.0", "GPL-2.0+"], 7 | "authors": [ 8 | { 9 | "name": "envms", 10 | "homepage": "https://env.ms" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Envms\\FluentPDO\\": "src/" 16 | } 17 | }, 18 | "require": { 19 | "php": ">=7.1", 20 | "ext-pdo": "*" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^8.0", 24 | "envms/fluent-test": "^1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/StructureTest.php: -------------------------------------------------------------------------------- 1 | getPrimaryKey('user')); 21 | self::assertEquals('user_id', $structure->getForeignKey('user')); 22 | } 23 | 24 | public function testCustomKey() 25 | { 26 | $structure = new Structure('whatAnId', '%s_\xid'); 27 | 28 | self::assertEquals('whatAnId', $structure->getPrimaryKey('user')); 29 | self::assertEquals('user_\xid', $structure->getForeignKey('user')); 30 | } 31 | 32 | public function testMethodKey() 33 | { 34 | $structure = new Structure('id', ['StructureTest', 'suffix']); 35 | 36 | self::assertEquals('id', $structure->getPrimaryKey('user')); 37 | self::assertEquals('user_id', $structure->getForeignKey('user')); 38 | } 39 | 40 | /** 41 | * @param $table 42 | * 43 | * @return string 44 | */ 45 | public static function suffix($table) 46 | { 47 | return $table . '_id'; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Structure.php: -------------------------------------------------------------------------------- 1 | primaryKey = $primaryKey; 28 | $this->foreignKey = $foreignKey; 29 | } 30 | 31 | /** 32 | * @param string $table 33 | * 34 | * @return string 35 | */ 36 | public function getPrimaryKey($table) 37 | { 38 | return $this->key($this->primaryKey, $table); 39 | } 40 | 41 | /** 42 | * @param string $table 43 | * 44 | * @return string 45 | */ 46 | public function getForeignKey($table) 47 | { 48 | return $this->key($this->foreignKey, $table); 49 | } 50 | 51 | /** 52 | * @param string|callback $key 53 | * @param string $table 54 | * 55 | * @return string 56 | */ 57 | private function key($key, $table) 58 | { 59 | if (is_callable($key)) { 60 | return $key($table); 61 | } 62 | 63 | return sprintf($key, $table); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Queries/Json.php: -------------------------------------------------------------------------------- 1 | ', ', 31 | 'JOIN' => [$this, 'getClauseJoin'], 32 | 'WHERE' => [$this, 'getClauseWhere'], 33 | 'GROUP BY' => ',', 34 | 'HAVING' => ' AND ', 35 | 'ORDER BY' => ', ', 36 | 'LIMIT' => null, 37 | 'OFFSET' => null, 38 | "\n--" => "\n--", 39 | ]; 40 | 41 | parent::__construct($fluent, $clauses); 42 | 43 | // initialize statements 44 | $tableParts = explode(' ', $table); 45 | $this->fromTable = reset($tableParts); 46 | $this->fromAlias = end($tableParts); 47 | 48 | $this->statements['SELECT'][] = ''; 49 | $this->joins[] = $this->fromAlias; 50 | 51 | if (isset($fluent->convertTypes) && $fluent->convertTypes) { 52 | $this->convertTypes = true; 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /tests/Queries/DeleteTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 24 | 25 | $this->fluent = new Query($pdo); 26 | } 27 | 28 | public function testDelete() 29 | { 30 | $query = $this->fluent->deleteFrom('user') 31 | ->where('id', 1); 32 | 33 | self::assertEquals('DELETE FROM user WHERE id = ?', $query->getQuery(false)); 34 | self::assertEquals(['0' => '1'], $query->getParameters()); 35 | } 36 | 37 | public function testDeleteIgnore() 38 | { 39 | $query = $this->fluent->deleteFrom('user') 40 | ->ignore() 41 | ->where('id', 1); 42 | 43 | self::assertEquals('DELETE IGNORE FROM user WHERE id = ?', $query->getQuery(false)); 44 | self::assertEquals(['0' => '1'], $query->getParameters()); 45 | } 46 | 47 | public function testDeleteOrderLimit() 48 | { 49 | $query = $this->fluent->deleteFrom('user') 50 | ->where('id', 2) 51 | ->orderBy('name') 52 | ->limit(1); 53 | 54 | self::assertEquals('DELETE FROM user WHERE id = ? ORDER BY name LIMIT 1', $query->getQuery(false)); 55 | self::assertEquals(['0' => '2'], $query->getParameters()); 56 | } 57 | 58 | public function testDeleteExpanded() 59 | { 60 | $query = $this->fluent->delete('t1, t2') 61 | ->from('t1') 62 | ->innerJoin('t2 ON t1.id = t2.id') 63 | ->innerJoin('t3 ON t2.id = t3.id') 64 | ->where('t1.id', 1); 65 | 66 | self::assertEquals('DELETE t1, t2 FROM t1 INNER JOIN t2 ON t1.id = t2.id INNER JOIN t3 ON t2.id = t3.id WHERE t1.id = ?', 67 | $query->getQuery(false)); 68 | self::assertEquals(['0' => '1'], $query->getParameters()); 69 | } 70 | 71 | public function testDeleteShortcut() 72 | { 73 | $query = $this->fluent->deleteFrom('user', 1); 74 | 75 | self::assertEquals('DELETE FROM user WHERE id = ?', $query->getQuery(false)); 76 | self::assertEquals(['0' => '1'], $query->getParameters()); 77 | } 78 | 79 | public function testAddFromAfterDelete() 80 | { 81 | $query = $this->fluent->delete('user', 1)->from('user'); 82 | 83 | self::assertEquals('DELETE user FROM user WHERE id = ?', $query->getQuery(false)); 84 | self::assertEquals(['0' => '1'], $query->getParameters()); 85 | } 86 | } -------------------------------------------------------------------------------- /tests/_resources/fluentdb.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS fluentdb; 2 | USE fluentdb; 3 | 4 | SET NAMES utf8; 5 | SET foreign_key_checks = 0; 6 | SET time_zone = 'SYSTEM'; 7 | SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; 8 | 9 | DROP TABLE IF EXISTS `article`; 10 | CREATE TABLE `article` ( 11 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 12 | `user_id` INT(10) UNSIGNED NOT NULL DEFAULT 0, 13 | `published_at` DATETIME NOT NULL DEFAULT 0, 14 | `title` VARCHAR(100) NOT NULL DEFAULT '', 15 | `content` TEXT NOT NULL DEFAULT '', 16 | PRIMARY KEY (`id`), 17 | KEY `user_id` (`user_id`), 18 | CONSTRAINT `fk_article_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) 19 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 20 | 21 | INSERT INTO `article` (`id`, `user_id`, `published_at`, `title`, `content`) VALUES 22 | (1, 1, '2011-12-10 12:10:00', 'article 1', 'content 1'), 23 | (2, 2, '2011-12-20 16:20:00', 'article 2', 'content 2'), 24 | (3, 1, '2012-01-04 22:00:00', 'article 3', 'content 3'), 25 | (4, 4, '2018-07-07 15:15:07', 'artïcle 4', 'content 4'), 26 | (5, 3, '2018-10-01 01:10:01', 'article 5', 'content 5'), 27 | (6, 3, '2019-01-21 07:00:00', 'სარედაქციო 6', '함유량 6'); 28 | 29 | DROP TABLE IF EXISTS `comment`; 30 | CREATE TABLE `comment` ( 31 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 32 | `article_id` INT(10) UNSIGNED NOT NULL, 33 | `user_id` INT(10) UNSIGNED NOT NULL, 34 | `content` VARCHAR(100) NOT NULL, 35 | PRIMARY KEY (`id`), 36 | KEY `article_id` (`article_id`), 37 | KEY `user_id` (`user_id`), 38 | CONSTRAINT `fk_comment_article_id` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`), 39 | CONSTRAINT `fk_comment_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 41 | 42 | INSERT INTO `comment` (`id`, `article_id`, `user_id`, `content`) VALUES 43 | (1, 1, 1, 'comment 1.1'), 44 | (2, 1, 2, 'comment 1.2'), 45 | (3, 2, 1, 'comment 2.1'), 46 | (4, 5, 4, 'cömment 5.4'), 47 | (5, 6, 2, 'ਟਿੱਪਣੀ 6.2'); 48 | 49 | DROP TABLE IF EXISTS `country`; 50 | CREATE TABLE `country` ( 51 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 52 | `name` VARCHAR(20) NOT NULL, 53 | `details` JSON NOT NULL, 54 | PRIMARY KEY (`id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 56 | 57 | INSERT INTO `country` (`id`, `name`, `details`) VALUES 58 | (1, 'Slovakia', '{"name": "Slovensko", "pop": 5456300, "gdp": 90.75}'), 59 | (2, 'Canada', '{"name": "Canada", "pop": 37198400, "gdp": 1592.37}'), 60 | (3, 'Germany', '{"name": "Deutschland", "pop": 82385700, "gdp": 3486.12}'); 61 | 62 | DROP TABLE IF EXISTS `user`; 63 | CREATE TABLE `user` ( 64 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 65 | `country_id` INT(10) UNSIGNED NOT NULL, 66 | `type` ENUM('admin','author') NOT NULL, 67 | `name` VARCHAR(20) NOT NULL, 68 | PRIMARY KEY (`id`), 69 | KEY `country_id` (`country_id`), 70 | CONSTRAINT `fk_user_country_id` FOREIGN KEY (`country_id`) REFERENCES `country` (`id`) 71 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 72 | 73 | INSERT INTO `user` (`id`, `country_id`, `type`, `name`) VALUES 74 | (1, 1, 'admin', 'Marek'), 75 | (2, 1, 'author', 'Robert'), 76 | (3, 2, 'admin', 'Chris'), 77 | (4, 2, 'author', 'Kevin'); 78 | 79 | -- 2018-10-01 07:42:17 80 | -------------------------------------------------------------------------------- /tests/UtilitiesTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 22 | 23 | $this->fluent = new Query($pdo); 24 | } 25 | 26 | public function testFluentUtil() 27 | { 28 | 29 | $value = Utilities::toUpperWords('one'); 30 | $value2 = Utilities::toUpperWords(' one '); 31 | $value3 = Utilities::toUpperWords('oneTwo'); 32 | $value4 = Utilities::toUpperWords('OneTwo'); 33 | $value5 = Utilities::toUpperWords('oneTwoThree'); 34 | $value6 = Utilities::toUpperWords(' oneTwoThree '); 35 | 36 | self::assertEquals('ONE', $value); 37 | self::assertEquals('ONE', $value2); 38 | self::assertEquals('ONE TWO', $value3); 39 | self::assertEquals('ONE TWO', $value4); 40 | self::assertEquals('ONE TWO THREE', $value5); 41 | self::assertEquals('ONE TWO THREE', $value6); 42 | } 43 | 44 | public function testFormatQuery() 45 | { 46 | $query = $this->fluent 47 | ->from('user') 48 | ->where('id > ?', 0) 49 | ->orderBy('name'); 50 | 51 | $formattedQuery = Utilities::formatQuery($query); 52 | 53 | self::assertEquals("SELECT user.*\nFROM user\nWHERE id > ?\nORDER BY name", $formattedQuery); 54 | } 55 | 56 | public function testConvertToNativeType() 57 | { 58 | $query = $this->fluent 59 | ->from('user') 60 | ->select(null) 61 | ->select(['id']) 62 | ->where('name', 'Marek') 63 | ->execute(); 64 | 65 | $returnRow = $query->fetch(); 66 | $forceInt = Utilities::stringToNumeric($query, $returnRow); 67 | 68 | self::assertEquals(['id' => '1'], $returnRow); 69 | self::assertEquals(['id' => 1], $forceInt); 70 | } 71 | 72 | public function testConvertSqlWriteValues() 73 | { 74 | $valueArray = Utilities::convertSqlWriteValues(['string', 1, 2, false, true, null, 'false']); 75 | $value1 = Utilities::convertSqlWriteValues(false); 76 | $value2 = Utilities::convertSqlWriteValues(true); 77 | 78 | self::assertEquals(['string', 1, 2, 0, 1, null, 'false'], $valueArray); 79 | self::assertEquals(0, $value1); 80 | self::assertEquals(1, $value2); 81 | } 82 | 83 | public function testisCountable() 84 | { 85 | $selectQuery = $this->fluent 86 | ->from('user') 87 | ->select(null) 88 | ->select(['id']) 89 | ->where('name', 'Marek'); 90 | 91 | $deleteQuery = $this->fluent 92 | ->deleteFrom('user') 93 | ->where('id', 1); 94 | 95 | self::assertEquals(true, Utilities::isCountable($selectQuery)); 96 | self::assertEquals(false, Utilities::isCountable($deleteQuery)); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/Queries/Delete.php: -------------------------------------------------------------------------------- 1 | [$this, 'getClauseDeleteFrom'], 33 | 'DELETE' => [$this, 'getClauseDelete'], 34 | 'FROM' => null, 35 | 'JOIN' => [$this, 'getClauseJoin'], 36 | 'WHERE' => [$this, 'getClauseWhere'], 37 | 'ORDER BY' => ', ', 38 | 'LIMIT' => null, 39 | ]; 40 | 41 | parent::__construct($fluent, $clauses); 42 | 43 | $this->statements['DELETE FROM'] = $table; 44 | $this->statements['DELETE'] = $table; 45 | } 46 | 47 | /** 48 | * Forces delete operation to fail silently 49 | * 50 | * @return Delete 51 | */ 52 | public function ignore() 53 | { 54 | $this->ignore = true; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @throws Exception 61 | * 62 | * @return string 63 | */ 64 | protected function buildQuery() 65 | { 66 | if ($this->statements['FROM']) { 67 | unset($this->clauses['DELETE FROM']); 68 | } else { 69 | unset($this->clauses['DELETE']); 70 | } 71 | 72 | return parent::buildQuery(); 73 | } 74 | 75 | /** 76 | * Execute DELETE query 77 | * 78 | * @throws Exception 79 | * 80 | * @return bool 81 | */ 82 | public function execute() 83 | { 84 | if (empty($this->statements['WHERE'])) { 85 | throw new Exception('Delete queries must contain a WHERE clause to prevent unwanted data loss'); 86 | } 87 | 88 | $result = parent::execute(); 89 | if ($result) { 90 | return $result->rowCount(); 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | protected function getClauseDelete() 100 | { 101 | return 'DELETE' . ($this->ignore ? " IGNORE" : '') . ' ' . $this->statements['DELETE']; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | protected function getClauseDeleteFrom() 108 | { 109 | return 'DELETE' . ($this->ignore ? " IGNORE" : '') . ' FROM ' . $this->statements['DELETE FROM']; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /tests/RegexTest.php: -------------------------------------------------------------------------------- 1 | regex = new Regex(); 21 | } 22 | 23 | public function testCamelCasedSpaced() 24 | { 25 | $name = $this->regex->camelCaseSpaced("magicCallMethod"); 26 | 27 | self::assertEquals("magic Call Method", $name); 28 | } 29 | 30 | public function testSplitClauses() 31 | { 32 | $query = $this->regex->splitClauses("SELECT * FROM user WHERE id = 1 OR id = 2 ORDER BY id ASC"); 33 | 34 | self::assertEquals("SELECT * \nFROM user \nWHERE id = 1 OR id = 2 \nORDER BY id ASC", $query); 35 | } 36 | 37 | public function testSplitSubClauses() 38 | { 39 | $query = $this->regex->splitSubClauses("SELECT * FROM user LEFT JOIN article WHERE 1 OR 2"); 40 | 41 | self::assertEquals("SELECT * FROM user \n LEFT JOIN article WHERE 1 \n OR 2", $query); 42 | } 43 | 44 | public function testRemoveLineEndWhitespace() 45 | { 46 | $query = $this->regex->removeLineEndWhitespace("SELECT * \n FROM user \n"); 47 | 48 | self::assertEquals("SELECT *\n FROM user\n", $query); 49 | } 50 | 51 | public function testRemoveAdditionalJoins() 52 | { 53 | $join = $this->regex->removeAdditionalJoins("user.article:id"); 54 | 55 | self::assertEquals("article.id", $join); 56 | } 57 | 58 | public function testSqlParameter() 59 | { 60 | $isParam = $this->regex->sqlParameter("id = :id"); 61 | self::assertEquals(1, $isParam); 62 | 63 | $isParam = $this->regex->sqlParameter("name = ?"); 64 | self::assertEquals(1, $isParam); 65 | 66 | $isParam = $this->regex->sqlParameter("count IN (22, 77)"); 67 | self::assertEquals(0, $isParam); 68 | } 69 | 70 | public function testTableAlias() 71 | { 72 | $isAlias = $this->regex->tableAlias("user AS u"); 73 | self::assertEquals(1, $isAlias); 74 | 75 | $isAlias = $this->regex->tableAlias("user.*"); 76 | self::assertEquals(1, $isAlias); 77 | 78 | $isAlias = $this->regex->tableAlias(" "); 79 | self::assertEquals(0, $isAlias); 80 | 81 | $isAlias = $this->regex->tableAlias("0.00 AS ཎ"); 82 | self::assertEquals(1, $isAlias); 83 | } 84 | 85 | public function testTableJoin() 86 | { 87 | $join = $this->regex->tableJoin("user"); 88 | self::assertEquals(1, $join); 89 | 90 | $join = $this->regex->tableJoin("`user`."); 91 | self::assertEquals(1, $join); 92 | 93 | $join = $this->regex->tableJoin("'''"); 94 | self::assertEquals(0, $join); 95 | 96 | $join = $this->regex->tableJoin("ឃឡឱ."); 97 | self::assertEquals(1, $join); 98 | } 99 | 100 | public function testTableJoinFull() 101 | { 102 | $join = $this->regex->tableJoinFull("user."); 103 | self::assertEquals(1, $join); 104 | 105 | $join = $this->regex->tableJoinFull("`user`.`column`"); 106 | self::assertEquals(1, $join); 107 | 108 | $join = $this->regex->tableJoinFull("user .column"); 109 | self::assertEquals(0, $join); 110 | 111 | $join = $this->regex->tableJoinFull("ㇽㇺㇴ.ㇱ"); 112 | self::assertEquals(1, $join); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /tests/Queries/InsertTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 24 | 25 | $this->fluent = new Query($pdo); 26 | } 27 | 28 | public function testInsertStatement() 29 | { 30 | $query = $this->fluent->insertInto('article', [ 31 | 'user_id' => 1, 32 | 'title' => 'new title', 33 | 'content' => 'new content' 34 | ]); 35 | 36 | self::assertEquals('INSERT INTO article (user_id, title, content) VALUES (?, ?, ?)', $query->getQuery(false)); 37 | self::assertEquals(['0' => '1', '1' => 'new title', '2' => 'new content'], $query->getParameters()); 38 | } 39 | 40 | public function testInsertUpdate() 41 | { 42 | $query = $this->fluent->insertInto('article', ['id' => 1]) 43 | ->onDuplicateKeyUpdate([ 44 | 'published_at' => '2011-12-10 12:10:00', 45 | 'title' => 'article 1b', 46 | 'content' => new Envms\FluentPDO\Literal('abs(-1)') // let's update with a literal and a parameter value 47 | ]); 48 | 49 | $q = $this->fluent->from('article', 1); 50 | 51 | $query2 = $this->fluent->insertInto('article', ['id' => 1]) 52 | ->onDuplicateKeyUpdate([ 53 | 'published_at' => '2011-12-10 12:10:00', 54 | 'title' => 'article 1', 55 | 'content' => 'content 1', 56 | ]); 57 | 58 | $q2 = $this->fluent->from('article', 1); 59 | 60 | self::assertEquals('INSERT INTO article (id) VALUES (?) ON DUPLICATE KEY UPDATE published_at = ?, title = ?, content = abs(-1)', $query->getQuery(false)); 61 | self::assertEquals([0 => '1', 1 => '2011-12-10 12:10:00', 2 => 'article 1b'], $query->getParameters()); 62 | self::assertEquals('last_inserted_id = 1', 'last_inserted_id = ' . $query->execute()); 63 | self::assertEquals(['id' => '1', 'user_id' => '1', 'published_at' => '2011-12-10 12:10:00', 'title' => 'article 1b', 'content' => '1'], 64 | $q->fetch()); 65 | self::assertEquals('last_inserted_id = 1', 'last_inserted_id = ' . $query2->execute()); 66 | self::assertEquals(['id' => '1', 'user_id' => '1', 'published_at' => '2011-12-10 12:10:00', 'title' => 'article 1', 'content' => 'content 1'], 67 | $q2->fetch()); 68 | } 69 | 70 | public function testInsertWithLiteral() 71 | { 72 | $query = $this->fluent->insertInto('article', 73 | [ 74 | 'user_id' => 1, 75 | 'updated_at' => new Envms\FluentPDO\Literal('NOW()'), 76 | 'title' => 'new title', 77 | 'content' => 'new content', 78 | ]); 79 | 80 | self::assertEquals('INSERT INTO article (user_id, updated_at, title, content) VALUES (?, NOW(), ?, ?)', $query->getQuery(false)); 81 | self::assertEquals(['0' => '1', '1' => 'new title', '2' => 'new content'], $query->getParameters()); 82 | } 83 | 84 | public function testInsertIgnore() 85 | { 86 | $query = $this->fluent->insertInto('article', 87 | [ 88 | 'user_id' => 1, 89 | 'title' => 'new title', 90 | 'content' => 'new content', 91 | ])->ignore(); 92 | 93 | self::assertEquals('INSERT IGNORE INTO article (user_id, title, content) VALUES (?, ?, ?)', $query->getQuery(false)); 94 | self::assertEquals(['0' => '1', '1' => 'new title', '2' => 'new content'], $query->getParameters()); 95 | } 96 | } -------------------------------------------------------------------------------- /src/Utilities.php: -------------------------------------------------------------------------------- 1 | camelCaseSpaced($string))); 21 | } 22 | 23 | /** 24 | * @param string $query 25 | * 26 | * @return string 27 | */ 28 | public static function formatQuery($query) 29 | { 30 | $regex = new Regex(); 31 | 32 | $query = $regex->splitClauses($query); 33 | $query = $regex->splitSubClauses($query); 34 | $query = $regex->removeLineEndWhitespace($query); 35 | 36 | return $query; 37 | } 38 | 39 | /** 40 | * Converts columns from strings to types according to PDOStatement::columnMeta() 41 | * 42 | * @param \PDOStatement $statement 43 | * @param array|\Traversable $rows - provided by PDOStatement::fetch with PDO::FETCH_ASSOC 44 | * 45 | * @return array|\Traversable 46 | */ 47 | public static function stringToNumeric(\PDOStatement $statement, $rows) 48 | { 49 | for ($i = 0; ($columnMeta = $statement->getColumnMeta($i)) !== false; $i++) { 50 | $type = $columnMeta['native_type']; 51 | 52 | switch ($type) { 53 | case 'DECIMAL': 54 | case 'DOUBLE': 55 | case 'FLOAT': 56 | case 'INT24': 57 | case 'LONG': 58 | case 'LONGLONG': 59 | case 'NEWDECIMAL': 60 | case 'SHORT': 61 | case 'TINY': 62 | if (isset($rows[$columnMeta['name']])) { 63 | $rows[$columnMeta['name']] = $rows[$columnMeta['name']] + 0; 64 | } else { 65 | if (is_array($rows) || $rows instanceof \Traversable) { 66 | foreach ($rows as &$row) { 67 | if (isset($row[$columnMeta['name']])) { 68 | $row[$columnMeta['name']] = $row[$columnMeta['name']] + 0; 69 | } 70 | } 71 | unset($row); 72 | } 73 | } 74 | break; 75 | default: 76 | // return as string 77 | break; 78 | } 79 | } 80 | 81 | return $rows; 82 | } 83 | 84 | /** 85 | * @param $value 86 | * 87 | * @return bool 88 | */ 89 | public static function convertSqlWriteValues($value) 90 | { 91 | if (is_array($value)) { 92 | foreach ($value as $k => $v) { 93 | $value[$k] = self::convertValue($v); 94 | } 95 | } else { 96 | $value = self::convertValue($value); 97 | } 98 | 99 | return $value; 100 | } 101 | 102 | /** 103 | * @param $value 104 | * 105 | * @return int|string 106 | */ 107 | public static function convertValue($value) 108 | { 109 | switch (gettype($value)) { 110 | case 'boolean': 111 | $conversion = ($value) ? 1 : 0; 112 | break; 113 | default: 114 | $conversion = $value; 115 | break; 116 | } 117 | 118 | return $conversion; 119 | } 120 | 121 | /** 122 | * @param $subject 123 | * 124 | * @return bool 125 | */ 126 | public static function isCountable($subject) 127 | { 128 | return (is_array($subject) || ($subject instanceof \Countable)); 129 | } 130 | 131 | /** 132 | * @param $value 133 | * 134 | * @return Literal|mixed 135 | */ 136 | public static function nullToLiteral($value) 137 | { 138 | if ($value === null) { 139 | return new Literal('NULL'); 140 | } 141 | 142 | return $value; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Queries/Update.php: -------------------------------------------------------------------------------- 1 | [$this, 'getClauseUpdate'], 30 | 'JOIN' => [$this, 'getClauseJoin'], 31 | 'SET' => [$this, 'getClauseSet'], 32 | 'WHERE' => [$this, 'getClauseWhere'], 33 | 'ORDER BY' => ', ', 34 | 'LIMIT' => null, 35 | ]; 36 | parent::__construct($fluent, $clauses); 37 | 38 | $this->statements['UPDATE'] = $table; 39 | 40 | $tableParts = explode(' ', $table); 41 | $this->joins[] = end($tableParts); 42 | } 43 | 44 | /** 45 | * In Update's case, parameters are not assigned until the query is built, since this method 46 | * 47 | * @param string|array $fieldOrArray 48 | * @param bool|string $value 49 | * 50 | * @throws Exception 51 | * 52 | * @return $this 53 | */ 54 | public function set($fieldOrArray, $value = false) 55 | { 56 | if (!$fieldOrArray) { 57 | return $this; 58 | } 59 | if (is_string($fieldOrArray) && $value !== false) { 60 | $this->statements['SET'][$fieldOrArray] = $value; 61 | } else { 62 | if (!is_array($fieldOrArray)) { 63 | throw new Exception('You must pass a value, or provide the SET list as an associative array. column => value'); 64 | } else { 65 | foreach ($fieldOrArray as $field => $value) { 66 | $this->statements['SET'][$field] = $value; 67 | } 68 | } 69 | } 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Execute update query 76 | * 77 | * @param boolean $getResultAsPdoStatement true to return the pdo statement instead of row count 78 | * 79 | * @throws Exception 80 | * 81 | * @return int|boolean|\PDOStatement 82 | */ 83 | public function execute($getResultAsPdoStatement = false) 84 | { 85 | if (empty($this->statements['WHERE'])) { 86 | throw new Exception('Update queries must contain a WHERE clause to prevent unwanted data loss'); 87 | } 88 | 89 | $result = parent::execute(); 90 | 91 | if ($getResultAsPdoStatement) { 92 | return $result; 93 | } 94 | 95 | if ($result) { 96 | return $result->rowCount(); 97 | } 98 | 99 | return false; 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | protected function getClauseUpdate() 106 | { 107 | return 'UPDATE ' . $this->statements['UPDATE']; 108 | } 109 | 110 | /** 111 | * @return string 112 | */ 113 | protected function getClauseSet() 114 | { 115 | $setArray = []; 116 | foreach ($this->statements['SET'] as $field => $value) { 117 | // named params are being used here 118 | if (is_array($value) && strpos(key($value), ':') === 0) { 119 | $key = key($value); 120 | $setArray[] = $field . ' = ' . $key; 121 | $this->parameters['SET'][$key] = $value[$key]; 122 | } 123 | elseif ($value instanceof Literal) { 124 | $setArray[] = $field . ' = ' . $value; 125 | } else { 126 | $setArray[] = $field . ' = ?'; 127 | $this->parameters['SET'][$field] = $value; 128 | } 129 | } 130 | 131 | return ' SET ' . implode(', ', $setArray); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Regex.php: -------------------------------------------------------------------------------- 1 | from('comment') 74 | ->where('article.published_at > ?', $date) 75 | ->orderBy('published_at DESC') 76 | ->limit(5); 77 | ``` 78 | 79 | which would build the query below: 80 | 81 | ```mysql 82 | SELECT comment.* 83 | FROM comment 84 | LEFT JOIN article ON article.id = comment.article_id 85 | WHERE article.published_at > ? 86 | ORDER BY article.published_at DESC 87 | LIMIT 5 88 | ``` 89 | 90 | To get data from the select, all we do is loop through the returned array: 91 | 92 | ```php 93 | foreach ($query as $row) { 94 | echo "$row['title']\n"; 95 | } 96 | ``` 97 | 98 | ## Using the Smart Join Builder 99 | 100 | Let's start with a traditional join, below: 101 | 102 | ```php 103 | $query = $fluent->from('article') 104 | ->leftJoin('user ON user.id = article.user_id') 105 | ->select('user.name'); 106 | ``` 107 | 108 | That's pretty verbose, and not very smart. If your tables use proper primary and foreign key names, you can shorten the above to: 109 | 110 | ```php 111 | $query = $fluent->from('article') 112 | ->leftJoin('user') 113 | ->select('user.name'); 114 | ``` 115 | 116 | That's better, but not ideal. However, it would be even easier to **not write any joins**: 117 | 118 | ```php 119 | $query = $fluent->from('article') 120 | ->select('user.name'); 121 | ``` 122 | 123 | Awesome, right? FluentPDO is able to build the join for you, by you prepending the foreign table name to the requested column. 124 | 125 | All three snippets above will create the exact same query: 126 | 127 | ```mysql 128 | SELECT article.*, user.name 129 | FROM article 130 | LEFT JOIN user ON user.id = article.user_id 131 | ``` 132 | 133 | ##### Close your connection 134 | 135 | Finally, it's always a good idea to free resources as soon as they are done with their duties: 136 | 137 | ```php 138 | $fluent->close(); 139 | ``` 140 | 141 | ## CRUD Query Examples 142 | 143 | ##### SELECT 144 | 145 | ```php 146 | $query = $fluent->from('article')->where('id', 1)->fetch(); 147 | $query = $fluent->from('user', 1)->fetch(); // shorter version if selecting one row by primary key 148 | ``` 149 | 150 | ##### INSERT 151 | 152 | ```php 153 | $values = array('title' => 'article 1', 'content' => 'content 1'); 154 | 155 | $query = $fluent->insertInto('article')->values($values)->execute(); 156 | $query = $fluent->insertInto('article', $values)->execute(); // shorter version 157 | ``` 158 | 159 | ##### UPDATE 160 | 161 | ```php 162 | $set = array('published_at' => new FluentLiteral('NOW()')); 163 | 164 | $query = $fluent->update('article')->set($set)->where('id', 1)->execute(); 165 | $query = $fluent->update('article', $set, 1)->execute(); // shorter version if updating one row by primary key 166 | ``` 167 | 168 | ##### DELETE 169 | 170 | ```php 171 | $query = $fluent->deleteFrom('article')->where('id', 1)->execute(); 172 | $query = $fluent->deleteFrom('article', 1)->execute(); // shorter version if deleting one row by primary key 173 | ``` 174 | 175 | ***Note**: INSERT, UPDATE and DELETE queries will only run after you call `->execute()`* 176 | 177 | ## License 178 | 179 | Free for commercial and non-commercial use under the [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) or [GPL 2.0](http://www.gnu.org/licenses/gpl-2.0.html) licenses. 180 | -------------------------------------------------------------------------------- /tests/Queries/UpdateTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 24 | 25 | $this->fluent = new Query($pdo); 26 | } 27 | 28 | public function testUpdate() 29 | { 30 | $query = $this->fluent->update('country')->set('name', 'aikavolS')->where('id', 1); 31 | $query->execute(); 32 | 33 | $query2 = $this->fluent->from('country')->where('id', 1); 34 | 35 | self::assertEquals('UPDATE country SET name = ? WHERE id = ?', $query->getQuery(false)); 36 | self::assertEquals(['0' => 'aikavolS', '1' => '1'], $query->getParameters()); 37 | self::assertEquals(['id' => '1', 'name' => 'aikavolS', 'details' => '{"gdp": 90.75, "pop": 5456300, "name": "Slovensko"}'], $query2->fetch()); 38 | 39 | $this->fluent->update('country')->set('name', 'Slovakia')->where('id', 1)->execute(); 40 | $query3 = $this->fluent->from('country')->where('id', 1); 41 | 42 | self::assertEquals(['id' => '1', 'name' => 'Slovakia', 'details' => '{"gdp": 90.75, "pop": 5456300, "name": "Slovensko"}'], $query3->fetch()); 43 | } 44 | 45 | public function testUpdateLiteral() 46 | { 47 | $query = $this->fluent->update('article')->set('published_at', new Envms\FluentPDO\Literal('NOW()'))->where('user_id', 1); 48 | 49 | self::assertEquals('UPDATE article SET published_at = NOW() WHERE user_id = ?', $query->getQuery(false)); 50 | self::assertEquals(['0' => '1'], $query->getParameters()); 51 | } 52 | 53 | public function testUpdateFromArray() 54 | { 55 | $query = $this->fluent->update('user')->set(['name' => 'keraM', '`type`' => 'author'])->where('id', 1); 56 | 57 | self::assertEquals('UPDATE user SET name = ?, `type` = ? WHERE id = ?', $query->getQuery(false)); 58 | self::assertEquals([0 => 'keraM', 1 => 'author', 2 => '1'], $query->getParameters()); 59 | } 60 | 61 | public function testUpdateLeftJoin() 62 | { 63 | $query = $this->fluent->update('user') 64 | ->outerJoin('country ON country.id = user.country_id') 65 | ->set(['name' => 'keraM', '`type`' => 'author']) 66 | ->where('id', 1); 67 | 68 | self::assertEquals('UPDATE user OUTER JOIN country ON country.id = user.country_id SET name = ?, `type` = ? WHERE id = ?', 69 | $query->getQuery(false)); 70 | self::assertEquals([0 => 'keraM', 1 => 'author', 2 => '1'], $query->getParameters()); 71 | } 72 | 73 | public function testUpdateSmartJoin() 74 | { 75 | $query = $this->fluent->update('user') 76 | ->set(['type' => 'author']) 77 | ->where('country.id', 1); 78 | 79 | self::assertEquals('UPDATE user LEFT JOIN country ON country.id = user.country_id SET type = ? WHERE country.id = ?', 80 | $query->getQuery(false)); 81 | self::assertEquals([0 => 'author', 1 => '1'], $query->getParameters()); 82 | } 83 | 84 | public function testUpdateOrderLimit() 85 | { 86 | $query = $this->fluent->update('user') 87 | ->set(['type' => 'author']) 88 | ->where('id', 2) 89 | ->orderBy('name') 90 | ->limit(1); 91 | 92 | self::assertEquals('UPDATE user SET type = ? WHERE id = ? ORDER BY name LIMIT 1', $query->getQuery(false)); 93 | self::assertEquals([0 => 'author', 1 => '2'], $query->getParameters()); 94 | } 95 | 96 | public function testUpdateShortCut() 97 | { 98 | $query = $this->fluent->update('user', ['type' => 'admin'], 1); 99 | 100 | self::assertEquals('UPDATE user SET type = ? WHERE id = ?', $query->getQuery(false)); 101 | self::assertEquals([0 => 'admin', 1 => '1'], $query->getParameters()); 102 | } 103 | 104 | public function testUpdateZero() 105 | { 106 | $this->fluent->update('article')->set('content', '')->where('id', 1)->execute(); 107 | $user = $this->fluent->from('article')->where('id', 1)->fetch(); 108 | 109 | $printQuery = "ID: {$user['id']} - content: {$user['content']}"; 110 | 111 | $this->fluent->update('article')->set('content', 'content 1')->where('id', 1)->execute(); 112 | 113 | $user2 = $this->fluent->from('article')->where('id', 1)->fetch(); 114 | 115 | $printQuery2 = "ID: {$user2['id']} - content: {$user2['content']}"; 116 | 117 | self::assertEquals('ID: 1 - content: ', $printQuery); 118 | self::assertEquals('ID: 1 - content: content 1', $printQuery2); 119 | } 120 | 121 | public function testUpdateWhere() 122 | { 123 | $query = $this->fluent->update('users') 124 | ->set("`users`.`active`", 1) 125 | ->where("`country`.`name`", 'Slovakia') 126 | ->where("`users`.`name`", 'Marek'); 127 | 128 | $query2 = $this->fluent->update('users') 129 | ->set("[users].[active]", 1) 130 | ->where("[country].[name]", 'Slovakia') 131 | ->where("[users].[name]", 'Marek'); 132 | 133 | self::assertEquals('UPDATE users LEFT JOIN country ON country.id = users.country_id SET `users`.`active` = ? WHERE `country`.`name` = ? AND `users`.`name` = ?', 134 | $query->getQuery(false)); 135 | self::assertEquals([0 => '1', 1 => 'Slovakia', 2 => 'Marek'], $query->getParameters()); 136 | self::assertEquals('UPDATE users LEFT JOIN country ON country.id = users.country_id SET [users].[active] = ? WHERE [country].[name] = ? AND [users].[name] = ?', 137 | $query2->getQuery(false)); 138 | self::assertEquals([0 => '1', 1 => 'Slovakia', 2 => 'Marek'], $query2->getParameters()); 139 | } 140 | 141 | public function testUpdateNamedParameters() 142 | { 143 | $query = $this->fluent->update('users') 144 | ->set("`users`.`active`", [':active' => 1]) 145 | ->where("`country`.`name` = :country", [':country' => 'Slovakia']); 146 | 147 | self::assertEquals('UPDATE users LEFT JOIN country ON country.id = users.country_id SET `users`.`active` = :active WHERE `country`.`name` = :country', 148 | $query->getQuery(false)); 149 | self::assertEquals([':active' => '1', ':country' => 'Slovakia'], $query->getParameters()); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/Queries/Select.php: -------------------------------------------------------------------------------- 1 | ', ', 28 | 'FROM' => null, 29 | 'JOIN' => [$this, 'getClauseJoin'], 30 | 'WHERE' => [$this, 'getClauseWhere'], 31 | 'GROUP BY' => ',', 32 | 'HAVING' => ' AND ', 33 | 'ORDER BY' => ', ', 34 | 'LIMIT' => null, 35 | 'OFFSET' => null, 36 | "\n--" => "\n--" 37 | ]; 38 | parent::__construct($fluent, $clauses); 39 | 40 | // initialize statements 41 | $fromParts = explode(' ', $from); 42 | $this->fromTable = reset($fromParts); 43 | $this->fromAlias = end($fromParts); 44 | 45 | $this->statements['FROM'] = $from; 46 | $this->statements['SELECT'][] = $this->fromAlias . '.*'; 47 | $this->joins[] = $this->fromAlias; 48 | } 49 | 50 | /** 51 | * @param mixed $columns 52 | * @param bool $overrideDefault 53 | * 54 | * @return $this 55 | */ 56 | public function select($columns, bool $overrideDefault = false) 57 | { 58 | if ($overrideDefault === true) { 59 | $this->resetClause('SELECT'); 60 | } elseif ($columns === null) { 61 | return $this->resetClause('SELECT'); 62 | } 63 | 64 | $this->addStatement('SELECT', $columns, []); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Return table name from FROM clause 71 | */ 72 | public function getFromTable() 73 | { 74 | return $this->fromTable; 75 | } 76 | 77 | /** 78 | * Return table alias from FROM clause 79 | */ 80 | public function getFromAlias() 81 | { 82 | return $this->fromAlias; 83 | } 84 | 85 | /** 86 | * Returns a single column 87 | * 88 | * @param int $columnNumber 89 | * 90 | * @throws Exception 91 | * 92 | * @return string 93 | */ 94 | public function fetchColumn(int $columnNumber = 0) 95 | { 96 | if (($s = $this->execute()) !== false) { 97 | return $s->fetchColumn($columnNumber); 98 | } 99 | 100 | return $s; 101 | } 102 | 103 | /** 104 | * Fetch first row or column 105 | * 106 | * @param string $column - column name or empty string for the whole row 107 | * @param int $cursorOrientation 108 | * 109 | * @throws Exception 110 | * 111 | * @return mixed string, array or false if there is no row 112 | */ 113 | public function fetch(?string $column = null, int $cursorOrientation = \PDO::FETCH_ORI_NEXT) 114 | { 115 | if ($this->result === null) { 116 | $this->execute(); 117 | } 118 | 119 | if ($this->result === false) { 120 | return false; 121 | } 122 | 123 | $row = $this->result->fetch($this->currentFetchMode, $cursorOrientation); 124 | 125 | if ($this->fluent->convertRead === true) { 126 | $row = Utilities::stringToNumeric($this->result, $row); 127 | } 128 | 129 | if ($row && $column !== null) { 130 | if (is_object($row)) { 131 | return $row->{$column}; 132 | } else { 133 | return $row[$column]; 134 | } 135 | } 136 | 137 | return $row; 138 | } 139 | 140 | /** 141 | * Fetch pairs 142 | * 143 | * @param $key 144 | * @param $value 145 | * @param $object 146 | * 147 | * @throws Exception 148 | * 149 | * @return array|\PDOStatement 150 | */ 151 | public function fetchPairs($key, $value, $object = false) 152 | { 153 | if (($s = $this->select("$key, $value", true)->asObject($object)->execute()) !== false) { 154 | return $s->fetchAll(\PDO::FETCH_KEY_PAIR); 155 | } 156 | 157 | return $s; 158 | } 159 | 160 | /** Fetch all row 161 | * 162 | * @param string $index - specify index column. Allows for data organization by field using 'field[]' 163 | * @param string $selectOnly - select columns which could be fetched 164 | * 165 | * @throws Exception 166 | * 167 | * @return array|bool - fetched rows 168 | */ 169 | public function fetchAll($index = '', $selectOnly = '') 170 | { 171 | $indexAsArray = strpos($index, '[]'); 172 | 173 | if ($indexAsArray !== false) { 174 | $index = str_replace('[]', '', $index); 175 | } 176 | 177 | if ($selectOnly) { 178 | $this->select($index . ', ' . $selectOnly, true); 179 | } 180 | 181 | if ($index) { 182 | return $this->buildSelectData($index, $indexAsArray); 183 | } else { 184 | if (($result = $this->execute()) !== false) { 185 | if ($this->fluent->convertRead === true) { 186 | return Utilities::stringToNumeric($result, $result->fetchAll()); 187 | } else { 188 | return $result->fetchAll(); 189 | } 190 | } 191 | 192 | return false; 193 | } 194 | } 195 | 196 | /** 197 | * \Countable interface doesn't break current select query 198 | * 199 | * @throws Exception 200 | * 201 | * @return int 202 | */ 203 | #[\ReturnTypeWillChange] 204 | public function count() 205 | { 206 | $fluent = clone $this; 207 | 208 | return (int)$fluent->select('COUNT(*)', true)->fetchColumn(); 209 | } 210 | 211 | /** 212 | * @throws Exception 213 | * 214 | * @return \ArrayIterator|\PDOStatement 215 | */ 216 | public function getIterator() 217 | { 218 | if ($this->fluent->convertRead === true) { 219 | return new \ArrayIterator($this->fetchAll()); 220 | } else { 221 | return $this->execute(); 222 | } 223 | } 224 | 225 | /** 226 | * @param $index 227 | * @param $indexAsArray 228 | * 229 | * @return array 230 | */ 231 | private function buildSelectData($index, $indexAsArray) 232 | { 233 | $data = []; 234 | 235 | foreach ($this as $row) { 236 | if (is_object($row)) { 237 | $key = $row->{$index}; 238 | } else { 239 | $key = $row[$index]; 240 | } 241 | 242 | if ($indexAsArray) { 243 | $data[$key][] = $row; 244 | } else { 245 | $data[$key] = $row; 246 | } 247 | } 248 | 249 | return $data; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/Queries/Insert.php: -------------------------------------------------------------------------------- 1 | [$this, 'getClauseInsertInto'], 36 | 'VALUES' => [$this, 'getClauseValues'], 37 | 'ON DUPLICATE KEY UPDATE' => [$this, 'getClauseOnDuplicateKeyUpdate'], 38 | ]; 39 | parent::__construct($fluent, $clauses); 40 | 41 | $this->statements['INSERT INTO'] = $table; 42 | $this->values($values); 43 | } 44 | 45 | /** 46 | * Force insert operation to fail silently 47 | * 48 | * @return Insert 49 | */ 50 | public function ignore() 51 | { 52 | $this->ignore = true; 53 | 54 | return $this; 55 | } 56 | 57 | /** Force insert operation delay support 58 | * 59 | * @return Insert 60 | */ 61 | public function delayed() 62 | { 63 | $this->delayed = true; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Add VALUES 70 | * 71 | * @param $values 72 | * 73 | * @return Insert 74 | * @throws Exception 75 | */ 76 | public function values($values) 77 | { 78 | if (!is_array($values)) { 79 | throw new Exception('Param VALUES for INSERT query must be array'); 80 | } 81 | 82 | $first = current($values); 83 | if (is_string(key($values))) { 84 | // is one row array 85 | $this->addOneValue($values); 86 | } elseif (is_array($first) && is_string(key($first))) { 87 | // this is multi values 88 | foreach ($values as $oneValue) { 89 | $this->addOneValue($oneValue); 90 | } 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Add ON DUPLICATE KEY UPDATE 98 | * 99 | * @param array $values 100 | * 101 | * @return Insert 102 | */ 103 | public function onDuplicateKeyUpdate($values) 104 | { 105 | $this->statements['ON DUPLICATE KEY UPDATE'] = array_merge( 106 | $this->statements['ON DUPLICATE KEY UPDATE'], $values 107 | ); 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Execute insert query 114 | * 115 | * @param mixed $sequence 116 | * 117 | * @throws Exception 118 | * 119 | * @return int|bool - Last inserted primary key 120 | */ 121 | public function execute($sequence = null) 122 | { 123 | $result = parent::execute(); 124 | 125 | if ($result) { 126 | return $this->fluent->getPdo()->lastInsertId($sequence); 127 | } 128 | 129 | return false; 130 | } 131 | 132 | /** 133 | * @param null $sequence 134 | * 135 | * @throws Exception 136 | * 137 | * @return bool 138 | */ 139 | public function executeWithoutId($sequence = null) 140 | { 141 | $result = parent::execute(); 142 | 143 | if ($result) { 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * @return string 152 | */ 153 | protected function getClauseInsertInto() 154 | { 155 | return 'INSERT' . ($this->ignore ? " IGNORE" : '') . ($this->delayed ? " DELAYED" : '') . ' INTO ' . $this->statements['INSERT INTO']; 156 | } 157 | 158 | /** 159 | * @return string 160 | */ 161 | protected function getClauseValues() 162 | { 163 | $valuesArray = []; 164 | foreach ($this->statements['VALUES'] as $rows) { 165 | // literals should not be parametrized. 166 | // They are commonly used to call engine functions or literals. 167 | // Eg: NOW(), CURRENT_TIMESTAMP etc 168 | $placeholders = array_map([$this, 'parameterGetValue'], $rows); 169 | $valuesArray[] = '(' . implode(', ', $placeholders) . ')'; 170 | } 171 | 172 | $columns = implode(', ', $this->columns); 173 | $values = implode(', ', $valuesArray); 174 | 175 | return " ($columns) VALUES $values"; 176 | } 177 | 178 | 179 | /** 180 | * @return string 181 | */ 182 | protected function getClauseOnDuplicateKeyUpdate() 183 | { 184 | $result = []; 185 | foreach ($this->statements['ON DUPLICATE KEY UPDATE'] as $key => $value) { 186 | $result[] = "$key = " . $this->parameterGetValue($value); 187 | } 188 | 189 | return ' ON DUPLICATE KEY UPDATE ' . implode(', ', $result); 190 | } 191 | 192 | /** 193 | * @param $param 194 | * 195 | * @return string 196 | */ 197 | protected function parameterGetValue($param) 198 | { 199 | return $param instanceof Literal ? (string)$param : '?'; 200 | } 201 | 202 | /** 203 | * Removes all Literal instances from the argument 204 | * since they are not to be used as PDO parameters but rather injected directly into the query 205 | * 206 | * @param $statements 207 | * 208 | * @return array 209 | */ 210 | protected function filterLiterals($statements) 211 | { 212 | $f = function ($item) { 213 | return !$item instanceof Literal; 214 | }; 215 | 216 | return array_map(function ($item) use ($f) { 217 | if (is_array($item)) { 218 | return array_filter($item, $f); 219 | } 220 | 221 | return $item; 222 | }, array_filter($statements, $f)); 223 | } 224 | 225 | /** 226 | * @return array 227 | */ 228 | protected function buildParameters(): array 229 | { 230 | $this->parameters = array_merge( 231 | $this->filterLiterals($this->statements['VALUES']), 232 | $this->filterLiterals($this->statements['ON DUPLICATE KEY UPDATE']) 233 | ); 234 | 235 | return parent::buildParameters(); 236 | } 237 | 238 | /** 239 | * @param array $oneValue 240 | * 241 | * @throws Exception 242 | */ 243 | private function addOneValue($oneValue) 244 | { 245 | // check if all $keys are strings 246 | foreach ($oneValue as $key => $value) { 247 | if (!is_string($key)) { 248 | throw new Exception('INSERT query: All keys of value array have to be strings.'); 249 | } 250 | } 251 | if (!$this->firstValue) { 252 | $this->firstValue = $oneValue; 253 | } 254 | if (!$this->columns) { 255 | $this->columns = array_keys($oneValue); 256 | } 257 | if ($this->columns != array_keys($oneValue)) { 258 | throw new Exception('INSERT query: All VALUES have to same keys (columns).'); 259 | } 260 | $this->statements['VALUES'][] = $oneValue; 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 57 | 58 | // if exceptions are already activated in PDO, activate them in Fluent as well 59 | if ($this->pdo->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION) { 60 | $this->throwExceptionOnError(true); 61 | } 62 | 63 | $this->structure = ($structure instanceof Structure) ? $structure : new Structure(); 64 | } 65 | 66 | /** 67 | * Create SELECT query from $table 68 | * 69 | * @param ?string $table - db table name 70 | * @param ?int $primaryKey - return one row by primary key 71 | * 72 | * @return Select 73 | * 74 | * @throws Exception 75 | */ 76 | public function from(?string $table = null, ?int $primaryKey = null): Select 77 | { 78 | $this->setTableName($table); 79 | $table = $this->getFullTableName(); 80 | 81 | $query = new Select($this, $table); 82 | 83 | if ($primaryKey !== null) { 84 | $tableTable = $query->getFromTable(); 85 | $tableAlias = $query->getFromAlias(); 86 | $primaryKeyName = $this->structure->getPrimaryKey($tableTable); 87 | $query = $query->where("$tableAlias.$primaryKeyName", $primaryKey); 88 | } 89 | 90 | return $query; 91 | } 92 | 93 | /** 94 | * Create INSERT INTO query 95 | * 96 | * @param ?string $table 97 | * @param array $values - accepts one or multiple rows, @see docs 98 | * 99 | * @return Insert 100 | * 101 | * @throws Exception 102 | */ 103 | public function insertInto(?string $table = null, array $values = []): Insert 104 | { 105 | $this->setTableName($table); 106 | $table = $this->getFullTableName(); 107 | 108 | return new Insert($this, $table, $values); 109 | } 110 | 111 | /** 112 | * Create UPDATE query 113 | * 114 | * @param ?string $table 115 | * @param array|string $set 116 | * @param ?int $primaryKey 117 | * 118 | * @return Update 119 | * 120 | * @throws Exception 121 | */ 122 | public function update(?string $table = null, $set = [], ?int $primaryKey = null): Update 123 | { 124 | $this->setTableName($table); 125 | $table = $this->getFullTableName(); 126 | 127 | $query = new Update($this, $table); 128 | 129 | $query->set($set); 130 | if ($primaryKey) { 131 | $primaryKeyName = $this->getStructure()->getPrimaryKey($this->table); 132 | $query = $query->where($primaryKeyName, $primaryKey); 133 | } 134 | 135 | return $query; 136 | } 137 | 138 | /** 139 | * Create DELETE query 140 | * 141 | * @param ?string $table 142 | * @param ?int $primaryKey delete only row by primary key 143 | * 144 | * @return Delete 145 | * 146 | * @throws Exception 147 | */ 148 | public function delete(?string $table = null, ?int $primaryKey = null): Delete 149 | { 150 | $this->setTableName($table); 151 | $table = $this->getFullTableName(); 152 | 153 | $query = new Delete($this, $table); 154 | 155 | if ($primaryKey) { 156 | $primaryKeyName = $this->getStructure()->getPrimaryKey($this->table); 157 | $query = $query->where($primaryKeyName, $primaryKey); 158 | } 159 | 160 | return $query; 161 | } 162 | 163 | /** 164 | * Create DELETE FROM query 165 | * 166 | * @param ?string $table 167 | * @param ?int $primaryKey 168 | * 169 | * @return Delete 170 | */ 171 | public function deleteFrom(?string $table = null, ?int $primaryKey = null): Delete 172 | { 173 | $args = func_get_args(); 174 | 175 | return call_user_func_array([$this, 'delete'], $args); 176 | } 177 | 178 | /** 179 | * @return PDO 180 | */ 181 | public function getPdo(): PDO 182 | { 183 | return $this->pdo; 184 | } 185 | 186 | /** 187 | * @return Structure 188 | */ 189 | public function getStructure(): Structure 190 | { 191 | return $this->structure; 192 | } 193 | 194 | /** 195 | * Closes the \PDO connection to the database 196 | */ 197 | public function close(): void 198 | { 199 | $this->pdo = null; 200 | } 201 | 202 | /** 203 | * Set table name comprised of prefix.separator.table 204 | * 205 | * @param ?string $table 206 | * @param string $prefix 207 | * @param string $separator 208 | * 209 | * @return $this 210 | * 211 | * @throws Exception 212 | */ 213 | public function setTableName(?string $table = '', string $prefix = '', string $separator = ''): Query 214 | { 215 | if ($table !== null) { 216 | $this->prefix = $prefix; 217 | $this->separator = $separator; 218 | $this->table = $table; 219 | } 220 | 221 | if ($this->getFullTableName() === '') { 222 | throw new Exception('Table name cannot be empty'); 223 | } 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * @return string 230 | */ 231 | public function getFullTableName(): string 232 | { 233 | return $this->prefix . $this->separator . $this->table; 234 | } 235 | 236 | /** 237 | * @return string 238 | */ 239 | public function getPrefix(): string 240 | { 241 | return $this->prefix; 242 | } 243 | 244 | /** 245 | * @return string 246 | */ 247 | public function getSeparator(): string 248 | { 249 | return $this->separator; 250 | } 251 | 252 | /** 253 | * @return string 254 | */ 255 | public function getTable(): string 256 | { 257 | return $this->table; 258 | } 259 | 260 | /** 261 | * @param bool $flag 262 | */ 263 | public function throwExceptionOnError(bool $flag): void 264 | { 265 | $this->exceptionOnError = $flag; 266 | } 267 | 268 | /** 269 | * @param bool $read 270 | * @param bool $write 271 | */ 272 | public function convertTypes(bool $read, bool $write): void 273 | { 274 | $this->convertRead = $read; 275 | $this->convertWrite = $write; 276 | } 277 | 278 | /** 279 | * @param bool $flag 280 | */ 281 | public function convertReadTypes(bool $flag): void 282 | { 283 | $this->convertRead = $flag; 284 | } 285 | 286 | /** 287 | * @param bool $flag 288 | */ 289 | public function convertWriteTypes(bool $flag): void 290 | { 291 | $this->convertWrite = $flag; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /tests/Queries/SelectTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 24 | 25 | $this->fluent = new Query($pdo); 26 | } 27 | 28 | public function testBasicQuery() 29 | { 30 | $query = $this->fluent 31 | ->from('user') 32 | ->where('id > ?', 0) 33 | ->orderBy('name'); 34 | 35 | $query = $query->where('name = ?', 'Marek'); 36 | 37 | self::assertEquals('SELECT user.* FROM user WHERE id > ? AND name = ? ORDER BY name', $query->getQuery(false)); 38 | self::assertEquals(['id' => '1', 'country_id' => '1', 'type' => 'admin', 'name' => 'Marek'], $query->fetch()); 39 | self::assertEquals([0 => 0, 1 => 'Marek'], $query->getParameters()); 40 | } 41 | 42 | public function testReturnQueryWithHaving() 43 | { 44 | 45 | $query = $this->fluent 46 | ->from('user') 47 | ->select(null) 48 | ->select('type, count(id) AS type_count') 49 | ->where('id > ?', 1) 50 | ->groupBy('type') 51 | ->having('type_count > ?', 1) 52 | ->orderBy('name'); 53 | 54 | self::assertEquals("SELECT type, count(id) AS type_count FROM user WHERE id > ? GROUP BY type HAVING type_count > ? ORDER BY name", 55 | $query->getQuery(false)); 56 | } 57 | 58 | public function testReturnParameterWithId() 59 | { 60 | $query = $this->fluent 61 | ->from('user', 2); 62 | 63 | self::assertEquals([0 => 2], $query->getParameters()); 64 | self::assertEquals('SELECT user.* FROM user WHERE user.id = ?', $query->getQuery(false)); 65 | } 66 | 67 | public function testFromWithAlias() 68 | { 69 | $query = $this->fluent->from('user author')->getQuery(false); 70 | $query2 = $this->fluent->from('user AS author')->getQuery(false); 71 | $query3 = $this->fluent->from('user AS author', 1)->getQuery(false); 72 | $query4 = $this->fluent->from('user AS author')->select('country.name')->getQuery(false); 73 | 74 | self::assertEquals('SELECT author.* FROM user author', $query); 75 | self::assertEquals('SELECT author.* FROM user AS author', $query2); 76 | self::assertEquals('SELECT author.* FROM user AS author WHERE author.id = ?', $query3); 77 | self::assertEquals('SELECT author.*, country.name FROM user AS author LEFT JOIN country ON country.id = user AS author.country_id', $query4); 78 | } 79 | 80 | public function testWhereArrayParameter() 81 | { 82 | $query = $this->fluent 83 | ->from('user') 84 | ->where([ 85 | 'id' => 2, 86 | 'type' => 'author' 87 | ]); 88 | 89 | self::assertEquals('SELECT user.* FROM user WHERE id = ? AND type = ?', $query->getQuery(false)); 90 | self::assertEquals([0 => 2, 1 => 'author'], $query->getParameters()); 91 | } 92 | 93 | public function testWhereColumnValue() 94 | { 95 | $query = $this->fluent->from('user') 96 | ->where('type', 'author'); 97 | 98 | self::assertEquals('SELECT user.* FROM user WHERE type = ?', $query->getQuery(false)); 99 | self::assertEquals([0 => 'author'], $query->getParameters()); 100 | } 101 | 102 | public function testWhereColumnNull() 103 | { 104 | $query = $this->fluent 105 | ->from('user') 106 | ->where('type', null); 107 | 108 | self::assertEquals('SELECT user.* FROM user WHERE type IS NULL', $query->getQuery(false)); 109 | } 110 | 111 | public function testWhereColumnArray() 112 | { 113 | $query = $this->fluent 114 | ->from('user') 115 | ->where('id', [1, 2, 3]); 116 | 117 | self::assertEquals('SELECT user.* FROM user WHERE id IN (1, 2, 3)', $query->getQuery(false)); 118 | self::assertEquals([], $query->getParameters()); 119 | } 120 | 121 | public function testWherePreparedArray() 122 | { 123 | $query = $this->fluent 124 | ->from('user') 125 | ->where('id IN (?, ?, ?)', [1, 2, 3]); 126 | 127 | self::assertEquals('SELECT user.* FROM user WHERE id IN (?, ?, ?)', $query->getQuery(false)); 128 | self::assertEquals([0 => 1, 1 => 2, 2 => 3], $query->getParameters()); 129 | } 130 | 131 | public function testWhereColumnName() 132 | { 133 | $query = $this->fluent->from('user') 134 | ->where('type = :type', [':type' => 'author']) 135 | ->where('id > :id AND name <> :name', [':id' => 3, ':name' => 'Marek']); 136 | 137 | $returnValue = ''; 138 | foreach ($query as $row) { 139 | $returnValue = $row['name']; 140 | } 141 | 142 | self::assertEquals('SELECT user.* FROM user WHERE type = :type AND id > :id AND name <> :name', $query->getQuery(false)); 143 | self::assertEquals([':type' => 'author', ':id' => 3, ':name' => 'Marek'], $query->getParameters()); 144 | self::assertEquals('Kevin', $returnValue); 145 | } 146 | 147 | public function testWhereOr() 148 | { 149 | $query = $this->fluent->from('comment') 150 | ->where('comment.id = :id', [':id' => 1]) 151 | ->whereOr('user.id = :userId', [':userId' => 2]); 152 | 153 | self::assertEquals('SELECT comment.* FROM comment LEFT JOIN user ON user.id = comment.user_id WHERE comment.id = :id OR user.id = :userId', 154 | $query->getQuery(false)); 155 | self::assertEquals([':id' => '1', ':userId' => '2'], $query->getParameters()); 156 | } 157 | 158 | public function testWhereReset() 159 | { 160 | $query = $this->fluent->from('user')->where('id > ?', 0)->orderBy('name'); 161 | $query = $query->where(null)->where('name = ?', 'Marek'); 162 | 163 | self::assertEquals('SELECT user.* FROM user WHERE name = ? ORDER BY name', $query->getQuery(false)); 164 | self::assertEquals(['0' => 'Marek'], $query->getParameters()); 165 | self::assertEquals(['id' => '1', 'country_id' => '1', 'type' => 'admin', 'name' => 'Marek'], $query->fetch()); 166 | } 167 | 168 | 169 | 170 | public function testSelectArrayParam() 171 | { 172 | $query = $this->fluent 173 | ->from('user') 174 | ->select(null) 175 | ->select(['id', 'name']) 176 | ->where('id < ?', 2); 177 | 178 | self::assertEquals('SELECT id, name FROM user WHERE id < ?', $query->getQuery(false)); 179 | self::assertEquals(['0' => '2'], $query->getParameters()); 180 | self::assertEquals(['id' => '1', 'name' => 'Marek'], $query->fetch()); 181 | } 182 | 183 | public function testGroupByArrayParam() 184 | { 185 | $query = $this->fluent 186 | ->from('user') 187 | ->select(null) 188 | ->select('count(*) AS total_count') 189 | ->groupBy(['id', 'name']); 190 | 191 | self::assertEquals('SELECT count(*) AS total_count FROM user GROUP BY id,name', $query->getQuery(false)); 192 | self::assertEquals(['total_count' => '1'], $query->fetch()); 193 | } 194 | 195 | public function testCountable() 196 | { 197 | $articles = $this->fluent 198 | ->from('article') 199 | ->select(null) 200 | ->select('title') 201 | ->where('id > 1') 202 | ->where('id < 4'); 203 | 204 | $count = count($articles); 205 | 206 | self::assertEquals(2, $count); 207 | self::assertEquals([0 => ['title' => 'article 2'], 1 => ['title' => 'article 3']], $articles->fetchAll()); 208 | } 209 | 210 | public function testWhereNotArray() 211 | { 212 | $query = $this->fluent->from('article')->where('NOT id', [1, 2]); 213 | 214 | self::assertEquals('SELECT article.* FROM article WHERE NOT id IN (1, 2)', $query->getQuery(false)); 215 | } 216 | 217 | public function testWhereColNameEscaped() 218 | { 219 | $query = $this->fluent->from('user') 220 | ->where('`type` = :type', [':type' => 'author']) 221 | ->where('`id` > :id AND `name` <> :name', [':id' => 3, ':name' => 'Marek']); 222 | 223 | $rowDisplay = ''; 224 | foreach ($query as $row) { 225 | $rowDisplay = $row['name']; 226 | } 227 | 228 | self::assertEquals('SELECT user.* FROM user WHERE `type` = :type AND `id` > :id AND `name` <> :name', $query->getQuery(false)); 229 | self::assertEquals([':type' => 'author', ':id' => '3', ':name' => 'Marek'], $query->getParameters()); 230 | self::assertEquals('Kevin', $rowDisplay); 231 | } 232 | 233 | public function testAliasesForClausesGroupbyOrderBy() 234 | { 235 | $query = $this->fluent->from('article')->group('user_id')->order('id'); 236 | 237 | self::assertEquals('SELECT article.* FROM article GROUP BY user_id ORDER BY id', $query->getQuery(false)); 238 | } 239 | 240 | public function testFetch() 241 | { 242 | $queryPrint = $this->fluent->from('user', 1)->fetch('name'); 243 | $queryPrint2 = $this->fluent->from('user', 1)->fetch(); 244 | $statement = $this->fluent->from('user', 5)->fetch(); 245 | $statement2 = $this->fluent->from('user', 5)->fetch('name'); 246 | 247 | self::assertEquals('Marek', $queryPrint); 248 | self::assertEquals(['id' => '1', 'country_id' => '1', 'type' => 'admin', 'name' => 'Marek'], $queryPrint2); 249 | self::assertEquals(false, $statement); 250 | self::assertEquals(false, $statement2); 251 | } 252 | 253 | public function testFetchPairsFetchAll() 254 | { 255 | $result = $this->fluent->from('user')->fetchPairs('id', 'name'); 256 | $result2 = $this->fluent->from('user')->fetchAll(); 257 | 258 | self::assertEquals(['1' => 'Marek', '2' => 'Robert', '3' => 'Chris', '4' => 'Kevin'], $result); 259 | self::assertEquals([ 260 | 0 => ['id' => '1', 'country_id' => '1', 'type' => 'admin', 'name' => 'Marek'], 261 | 1 => ['id' => '2', 'country_id' => '1', 'type' => 'author', 'name' => 'Robert'], 262 | 2 => ['id' => '3', 'country_id' => '2', 'type' => 'admin', 'name' => 'Chris'], 263 | 3 => ['id' => '4', 'country_id' => '2', 'type' => 'author', 'name' => 'Kevin'] 264 | ], $result2); 265 | } 266 | 267 | public function testFetchAllWithParams() 268 | { 269 | $result = $this->fluent->from('user')->fetchAll('id', 'type, name'); 270 | 271 | self::assertEquals([1 => ['id' => '1', 'type' => 'admin', 'name' => 'Marek'], 2 => ['id' => '2', 'type' => 'author', 'name' => 'Robert'], 272 | 3 => ['id' => '3', 'type' => 'admin', 'name' => 'Chris'], 4 => ['id' => '4', 'type' => 'author', 'name' => 'Kevin']], 273 | $result); 274 | } 275 | 276 | public function testFetchColumn() 277 | { 278 | $printColumn = $this->fluent->from('user', 3)->fetchColumn(); 279 | $printColumn2 = $this->fluent->from('user', 3)->fetchColumn(3); 280 | $statement = $this->fluent->from('user', 5)->fetchColumn(); 281 | $statement2 = $this->fluent->from('user', 5)->fetchColumn(3); 282 | 283 | self::assertEquals(3, $printColumn); 284 | self::assertEquals('Chris', $printColumn2); 285 | self::assertEquals(false, $statement); 286 | self::assertEquals(false, $statement2); 287 | } 288 | } -------------------------------------------------------------------------------- /tests/Queries/CommonTest.php: -------------------------------------------------------------------------------- 1 | setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_BOTH); 25 | 26 | $this->fluent = new Query($pdo); 27 | } 28 | 29 | public function testFullJoin() 30 | { 31 | $query = $this->fluent->from('article') 32 | ->select('user.name') 33 | ->leftJoin('user ON user.id = article.user_id') 34 | ->orderBy('article.title'); 35 | 36 | $returnValue = ''; 37 | foreach ($query as $row) { 38 | $returnValue .= "$row[name] - $row[title] "; 39 | } 40 | 41 | self::assertEquals('SELECT article.*, user.name FROM article LEFT JOIN user ON user.id = article.user_id ORDER BY article.title', 42 | $query->getQuery(false)); 43 | self::assertEquals('Marek - article 1 Robert - article 2 Marek - article 3 Kevin - artïcle 4 Chris - article 5 Chris - სარედაქციო 6 ', $returnValue); 44 | } 45 | 46 | public function testShortJoin() 47 | { 48 | 49 | $query = $this->fluent->from('article')->leftJoin('user'); 50 | $query2 = $this->fluent->from('article')->leftJoin('user author'); 51 | $query3 = $this->fluent->from('article')->leftJoin('user AS author'); 52 | 53 | self::assertEquals('SELECT article.* FROM article LEFT JOIN user ON user.id = article.user_id', $query->getQuery(false)); 54 | self::assertEquals('SELECT article.* FROM article LEFT JOIN user AS author ON author.id = article.user_id', $query2->getQuery(false)); 55 | self::assertEquals('SELECT article.* FROM article LEFT JOIN user AS author ON author.id = article.user_id', $query3->getQuery(false)); 56 | } 57 | 58 | public function testJoinShortBackRef() 59 | { 60 | $query = $this->fluent->from('user')->innerJoin('article:'); 61 | $query2 = $this->fluent->from('user')->innerJoin('article: with_articles'); 62 | $query3 = $this->fluent->from('user')->innerJoin('article: AS with_articles'); 63 | 64 | self::assertEquals('SELECT user.* FROM user INNER JOIN article ON article.user_id = user.id', $query->getQuery(false)); 65 | self::assertEquals('SELECT user.* FROM user INNER JOIN article AS with_articles ON with_articles.user_id = user.id', 66 | $query2->getQuery(false)); 67 | self::assertEquals('SELECT user.* FROM user INNER JOIN article AS with_articles ON with_articles.user_id = user.id', 68 | $query3->getQuery(false)); 69 | } 70 | 71 | public function testJoinShortMulti() 72 | { 73 | $query = $this->fluent->from('comment') 74 | ->leftJoin('article.user'); 75 | 76 | self::assertEquals('SELECT comment.* FROM comment LEFT JOIN article ON article.id = comment.article_id LEFT JOIN user ON user.id = article.user_id', 77 | $query->getQuery(false)); 78 | } 79 | 80 | public function testJoinMultiBackRef() 81 | { 82 | $query = $this->fluent->from('article') 83 | ->innerJoin('comment:user AS comment_user'); 84 | 85 | self::assertEquals('SELECT article.* FROM article INNER JOIN comment ON comment.article_id = article.id INNER JOIN user AS comment_user ON comment_user.id = comment.user_id', 86 | $query->getQuery(false)); 87 | self::assertEquals(['id' => '1', 'user_id' => '1', 'published_at' => '2011-12-10 12:10:00', 'title' => 'article 1', 'content' => 'content 1'], 88 | $query->fetch()); 89 | } 90 | 91 | public function testJoinShortTwoSameTable() 92 | { 93 | $query = $this->fluent->from('article') 94 | ->leftJoin('user') 95 | ->leftJoin('user'); 96 | 97 | self::assertEquals('SELECT article.* FROM article LEFT JOIN user ON user.id = article.user_id', $query->getQuery(false)); 98 | } 99 | 100 | public function testJoinShortTwoTables() 101 | { 102 | $query = $this->fluent->from('comment') 103 | ->where('comment.id', 2) 104 | ->leftJoin('user comment_author')->select('comment_author.name AS comment_name') 105 | ->leftJoin('article.user AS article_author')->select('article_author.name AS author_name'); 106 | 107 | self::assertEquals('SELECT comment.*, comment_author.name AS comment_name, article_author.name AS author_name FROM comment LEFT JOIN user AS comment_author ON comment_author.id = comment.user_id LEFT JOIN article ON article.id = comment.article_id LEFT JOIN user AS article_author ON article_author.id = article.user_id WHERE comment.id = ?', 108 | $query->getQuery(false)); 109 | self::assertEquals([ 110 | 'id' => '2', 111 | 'article_id' => '1', 112 | 'user_id' => '2', 113 | 'content' => 'comment 1.2', 114 | 'comment_name' => 'Robert', 115 | 'author_name' => 'Marek' 116 | ], $query->fetch()); 117 | } 118 | 119 | public function testJoinInWhere() 120 | { 121 | $query = $this->fluent->from('article')->where('comment:content <> "" AND user.country.id = ?', 1); 122 | 123 | self::assertEquals('SELECT article.* FROM article LEFT JOIN comment ON comment.article_id = article.id LEFT JOIN user ON user.id = article.user_id LEFT JOIN country ON country.id = user.country_id WHERE comment.content <> "" AND country.id = ?', 124 | $query->getQuery(false)); 125 | } 126 | 127 | public function testJoinInSelect() 128 | { 129 | $query = $this->fluent->from('article')->select('user.name AS author'); 130 | 131 | self::assertEquals('SELECT article.*, user.name AS author FROM article LEFT JOIN user ON user.id = article.user_id', $query->getQuery(false)); 132 | } 133 | 134 | public function testJoinInOrderBy() 135 | { 136 | $query = $this->fluent->from('article')->orderBy('user.name, article.title'); 137 | 138 | self::assertEquals('SELECT article.* FROM article LEFT JOIN user ON user.id = article.user_id ORDER BY user.name, article.title', 139 | $query->getQuery(false)); 140 | } 141 | 142 | public function testJoinInGroupBy() 143 | { 144 | $query = $this->fluent->from('article')->groupBy('user.type') 145 | ->select(null)->select('user.type, count(article.id) AS article_count'); 146 | 147 | self::assertEquals('SELECT user.type, count(article.id) AS article_count FROM article LEFT JOIN user ON user.id = article.user_id GROUP BY user.type', 148 | $query->getQuery(false)); 149 | self::assertEquals(['0' => ['type' => 'admin', 'article_count' => '4'], '1' => ['type' => 'author', 'article_count' => '2']], 150 | $query->fetchAll()); 151 | } 152 | 153 | public function testEscapeJoin() 154 | { 155 | $query = $this->fluent->from('article') 156 | ->where('user\.name = ?', 'Chris'); 157 | 158 | self::assertEquals('SELECT article.* FROM article WHERE user.name = ?', $query->getQuery(false)); 159 | 160 | $query = $this->fluent->from('article') 161 | ->where('comment.id = :id', 1) 162 | ->where('user\.name = :name', 'Chris'); 163 | 164 | self::assertEquals('SELECT article.* FROM article LEFT JOIN comment ON comment.id = article.comment_id WHERE comment.id = :id AND user.name = :name', 165 | $query->getQuery(false)); 166 | } 167 | 168 | public function testDontCreateDuplicateJoins() 169 | { 170 | $query = $this->fluent->from('article') 171 | ->innerJoin('user AS author ON article.user_id = author.id') 172 | ->select('author.name'); 173 | 174 | $query2 = $this->fluent->from('article') 175 | ->innerJoin('user ON article.user_id = user.id') 176 | ->select('user.name'); 177 | 178 | $query3 = $this->fluent->from('article') 179 | ->innerJoin('user AS author ON article.user_id = author.id') 180 | ->select('author.country.name'); 181 | 182 | $query4 = $this->fluent->from('article') 183 | ->innerJoin('user ON article.user_id = user.id') 184 | ->select('user.country.name'); 185 | 186 | self::assertEquals('SELECT article.*, author.name FROM article INNER JOIN user AS author ON article.user_id = author.id', 187 | $query->getQuery(false)); 188 | self::assertEquals('SELECT article.*, user.name FROM article INNER JOIN user ON article.user_id = user.id', $query2->getQuery(false)); 189 | self::assertEquals('SELECT article.*, country.name FROM article INNER JOIN user AS author ON article.user_id = author.id LEFT JOIN country ON country.id = author.country_id', 190 | $query3->getQuery(false)); 191 | self::assertEquals('SELECT article.*, country.name FROM article INNER JOIN user ON article.user_id = user.id LEFT JOIN country ON country.id = user.country_id', 192 | $query4->getQuery(false)); 193 | } 194 | 195 | public function testClauseWithRefBeforeJoin() 196 | { 197 | $query = $this->fluent->from('article')->select('user.name')->innerJoin('user'); 198 | $query2 = $this->fluent->from('article')->select('author.name')->innerJoin('user AS author'); 199 | $query3 = $this->fluent->from('user')->select('article:title')->innerJoin('article:'); 200 | 201 | self::assertEquals('SELECT article.*, user.name FROM article INNER JOIN user ON user.id = article.user_id', $query->getQuery(false)); 202 | self::assertEquals('SELECT article.*, author.name FROM article INNER JOIN user AS author ON author.id = article.user_id', 203 | $query2->getQuery(false)); 204 | self::assertEquals('SELECT user.*, article.title FROM user INNER JOIN article ON article.user_id = user.id', $query3->getQuery(false)); 205 | } 206 | 207 | public function testFromOtherDB() 208 | { 209 | $queryPrint = $this->fluent->from('db2.user')->where('db2.user.name', 'name')->order('db2.user.name')->getQuery(false); 210 | 211 | self::assertEquals('SELECT db2.user.* FROM db2.user WHERE db2.user.name = ? ORDER BY db2.user.name', $queryPrint); 212 | } 213 | 214 | public function testJoinTableWithUsing() 215 | { 216 | $query = $this->fluent->from('article') 217 | ->innerJoin('user USING (user_id)') 218 | ->select('user.*') 219 | ->getQuery(false); 220 | 221 | $query2 = $this->fluent->from('article') 222 | ->innerJoin('user u USING (user_id)') 223 | ->select('u.*') 224 | ->getQuery(false); 225 | 226 | $query3 = $this->fluent->from('article') 227 | ->innerJoin('user AS u USING (user_id)') 228 | ->select('u.*') 229 | ->getQuery(false); 230 | 231 | self::assertEquals('SELECT article.*, user.* FROM article INNER JOIN user USING (user_id)', $query); 232 | self::assertEquals('SELECT article.*, u.* FROM article INNER JOIN user u USING (user_id)', $query2); 233 | self::assertEquals('SELECT article.*, u.* FROM article INNER JOIN user AS u USING (user_id)', $query3); 234 | } 235 | 236 | public function testDisableSmartJoin() 237 | { 238 | $query = $this->fluent->from('comment') 239 | ->select('user.name') 240 | ->orderBy('article.published_at') 241 | ->getQuery(false); 242 | $printQuery = "-- Plain: $query"; 243 | 244 | $query2 = $this->fluent->from('comment') 245 | ->select('user.name') 246 | ->disableSmartJoin() 247 | ->orderBy('article.published_at') 248 | ->getQuery(false); 249 | 250 | $printQuery2 = "-- Disable: $query2"; 251 | 252 | $query3 = $this->fluent->from('comment') 253 | ->disableSmartJoin() 254 | ->select('user.name') 255 | ->enableSmartJoin() 256 | ->orderBy('article.published_at') 257 | ->getQuery(false); 258 | $printQuery3 = "-- Disable and enable: $query3"; 259 | 260 | self::assertEquals('-- Plain: SELECT comment.*, user.name FROM comment LEFT JOIN user ON user.id = comment.user_id LEFT JOIN article ON article.id = comment.article_id ORDER BY article.published_at', 261 | $printQuery); 262 | self::assertEquals('-- Disable: SELECT comment.*, user.name FROM comment ORDER BY article.published_at', $printQuery2); 263 | self::assertEquals('-- Disable and enable: SELECT comment.*, user.name FROM comment LEFT JOIN user ON user.id = comment.user_id LEFT JOIN article ON article.id = comment.article_id ORDER BY article.published_at', 264 | $printQuery3); 265 | } 266 | 267 | public function testPDOFetchObj() 268 | { 269 | $query = $this->fluent->from('user')->where('id > ?', 0)->orderBy('name'); 270 | $query = $query->where('name = ?', 'Marek'); 271 | $this->fluent->getPdo()->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); 272 | 273 | $expectObj = new stdClass(); 274 | $expectObj->id = 1; 275 | $expectObj->country_id = 1; 276 | $expectObj->type = 'admin'; 277 | $expectObj->name = 'Marek'; 278 | 279 | self::assertEquals(['0' => '0', '1' => 'Marek'], $query->getParameters()); 280 | self::assertEquals($expectObj, $query->fetch()); 281 | } 282 | 283 | public function testFromIdAsObject() 284 | { 285 | $query = $this->fluent->from('user', 2)->asObject(); 286 | 287 | $expectObj = new stdClass(); 288 | $expectObj->id = 2; 289 | $expectObj->country_id = 1; 290 | $expectObj->type = 'author'; 291 | $expectObj->name = 'Robert'; 292 | 293 | self::assertEquals('SELECT user.* FROM user WHERE user.id = ?', $query->getQuery(false)); 294 | self::assertEquals($expectObj, $query->fetch()); 295 | } 296 | 297 | public function testFromIdAsObjectUser() 298 | { 299 | $expectedUser = new User(); 300 | $expectedUser->id = 2; 301 | $expectedUser->country_id = 1; 302 | $expectedUser->type = 'author'; 303 | $expectedUser->name = 'Robert'; 304 | 305 | $query = $this->fluent->from('user', 2)->asObject(User::class); 306 | $user = $query->fetch(); 307 | 308 | self::assertEquals('SELECT user.* FROM user WHERE user.id = ?', $query->getQuery(false)); 309 | self::assertEquals($expectedUser, $user); 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /src/Queries/Common.php: -------------------------------------------------------------------------------- 1 | validMethods)) { 63 | trigger_error("Call to invalid method " . get_class($this) . "::{$name}()", E_USER_ERROR); 64 | } 65 | 66 | $clause = Utilities::toUpperWords($name); 67 | 68 | if ($clause == 'GROUP' || $clause == 'ORDER') { 69 | $clause = "{$clause} BY"; 70 | } 71 | 72 | if ($clause == 'COMMENT') { 73 | $clause = "\n--"; 74 | } 75 | 76 | $statement = array_shift($parameters); 77 | 78 | if (strpos($clause, 'JOIN') !== false) { 79 | return $this->addJoinStatements($clause, $statement, $parameters); 80 | } 81 | 82 | return $this->addStatement($clause, $statement, $parameters); 83 | } 84 | 85 | /** 86 | * @return $this 87 | */ 88 | public function enableSmartJoin() 89 | { 90 | $this->isSmartJoinEnabled = true; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return $this 97 | */ 98 | public function disableSmartJoin() 99 | { 100 | $this->isSmartJoinEnabled = false; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @return bool 107 | */ 108 | public function isSmartJoinEnabled() 109 | { 110 | return $this->isSmartJoinEnabled; 111 | } 112 | 113 | /** 114 | * Add where condition, defaults to appending with AND 115 | * 116 | * @param string|array $condition - possibly containing ? or :name (PDO syntax) 117 | * @param mixed $parameters 118 | * @param string $separator - should be AND or OR 119 | * 120 | * @return $this 121 | */ 122 | public function where($condition, $parameters = [], $separator = 'AND') 123 | { 124 | if ($condition === null) { 125 | return $this->resetClause('WHERE'); 126 | } 127 | 128 | if (!$condition) { 129 | return $this; 130 | } 131 | 132 | if (is_array($condition)) { // where(["column1 > ?" => 1, "column2 < ?" => 2]) 133 | foreach ($condition as $key => $val) { 134 | $this->where($key, $val); 135 | } 136 | 137 | return $this; 138 | } 139 | 140 | $args = func_get_args(); 141 | 142 | if ($parameters === []) { 143 | return $this->addWhereStatement($condition, $separator); 144 | } 145 | 146 | /* 147 | * Check that there are 2 arguments, a condition and a parameter value. If the condition contains 148 | * a parameter (? or :name), add them; it's up to the dev to be valid sql. Otherwise it's probably 149 | * just an identifier, so construct a new condition based on the passed parameter value. 150 | */ 151 | if (count($args) >= 2 && !$this->regex->sqlParameter($condition)) { 152 | // condition is column only 153 | if (is_null($parameters)) { 154 | return $this->addWhereStatement("$condition IS NULL", $separator); 155 | } elseif ($args[1] === []) { 156 | return $this->addWhereStatement('FALSE', $separator); 157 | } elseif (is_array($args[1])) { 158 | $in = $this->quote($args[1]); 159 | 160 | return $this->addWhereStatement("$condition IN $in", $separator); 161 | } 162 | 163 | // don't parameterize the value if it's an instance of Literal 164 | if ($parameters instanceof Literal) { 165 | $condition = "{$condition} = {$parameters}"; 166 | 167 | return $this->addWhereStatement($condition, $separator); 168 | } else { 169 | $condition = "$condition = ?"; 170 | } 171 | } 172 | 173 | $args = [0 => $args[1]]; 174 | 175 | // parameters can be passed as [1, 2, 3] and it will fill a condition like: id IN (?, ?, ?) 176 | if (is_array($parameters) && !empty($parameters)) { 177 | $args = $parameters; 178 | } 179 | 180 | return $this->addWhereStatement($condition, $separator, $args); 181 | } 182 | 183 | /** 184 | * Add where appending with OR 185 | * 186 | * @param string $condition - possibly containing ? or :name (PDO syntax) 187 | * @param mixed $parameters 188 | * 189 | * @return $this 190 | */ 191 | public function whereOr($condition, $parameters = []) 192 | { 193 | if (is_array($condition)) { // where(["column1 > ?" => 1, "column2 < ?" => 2]) 194 | foreach ($condition as $key => $val) { 195 | $this->whereOr($key, $val); 196 | } 197 | 198 | return $this; 199 | } 200 | 201 | return $this->where($condition, $parameters, 'OR'); 202 | } 203 | 204 | /** 205 | * @return string 206 | */ 207 | protected function getClauseJoin() 208 | { 209 | return implode(' ', $this->statements['JOIN']); 210 | } 211 | 212 | /** 213 | * @return string 214 | */ 215 | protected function getClauseWhere() { 216 | $firstStatement = array_shift($this->statements['WHERE']); 217 | $query = " WHERE {$firstStatement[1]}"; // append first statement to WHERE without condition 218 | 219 | if (!empty($this->statements['WHERE'])) { 220 | foreach ($this->statements['WHERE'] as $statement) { 221 | $query .= " {$statement[0]} {$statement[1]}"; // [0] -> AND/OR [1] -> field = ? 222 | } 223 | } 224 | 225 | // put the first statement back onto the beginning of the array in case we want to run this again 226 | array_unshift($this->statements['WHERE'], $firstStatement); 227 | 228 | return $query; 229 | } 230 | 231 | /** 232 | * Statement can contain more tables (e.g. "table1.table2:table3:") 233 | * 234 | * @param $clause 235 | * @param $statement 236 | * @param array $parameters 237 | * 238 | * @return $this 239 | */ 240 | private function addJoinStatements($clause, $statement, $parameters = []) 241 | { 242 | if ($statement === null) { 243 | $this->joins = []; 244 | 245 | return $this->resetClause('JOIN'); 246 | } 247 | 248 | if (array_search(substr($statement, 0, -1), $this->joins) !== false) { 249 | return $this; 250 | } 251 | 252 | list($joinAlias, $joinTable) = $this->setJoinNameAlias($statement); 253 | 254 | if (strpos(strtoupper($statement), ' ON ') !== false || strpos(strtoupper($statement), ' USING') !== false) { 255 | return $this->addRawJoins($clause, $statement, $parameters, $joinAlias, $joinTable); 256 | } 257 | 258 | $mainTable = $this->setMainTable(); 259 | 260 | // if $joinTable does not end with a dot or colon, append one 261 | if (!in_array(substr($joinTable, -1), ['.', ':'])) { 262 | $joinTable .= '.'; 263 | } 264 | 265 | $this->regex->tableJoin($joinTable, $matches); 266 | 267 | // used for applying the table alias 268 | $lastItem = array_pop($matches[1]); 269 | array_push($matches[1], $lastItem); 270 | 271 | foreach ($matches[1] as $joinItem) { 272 | if ($this->matchTableWithJoin($mainTable, $joinItem)) { 273 | // this is still the same table so we don't need to add the same join 274 | continue; 275 | } 276 | 277 | $mainTable = $this->applyTableJoin($clause, $parameters, $mainTable, $joinItem, $lastItem, $joinAlias); 278 | } 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * Create join string 285 | * 286 | * @param $clause 287 | * @param $mainTable 288 | * @param $joinTable 289 | * @param string $joinAlias 290 | * 291 | * @return string 292 | */ 293 | private function createJoinStatement($clause, $mainTable, $joinTable, $joinAlias = '') 294 | { 295 | if (in_array(substr($mainTable, -1), [':', '.'])) { 296 | $mainTable = substr($mainTable, 0, -1); 297 | } 298 | 299 | $referenceDirection = substr($joinTable, -1); 300 | $joinTable = substr($joinTable, 0, -1); 301 | $asJoinAlias = ''; 302 | 303 | if (!empty($joinAlias)) { 304 | $asJoinAlias = " AS $joinAlias"; 305 | } else { 306 | $joinAlias = $joinTable; 307 | } 308 | 309 | if (in_array($joinAlias, $this->joins)) { // if the join exists don't create it again 310 | return ''; 311 | } else { 312 | $this->joins[] = $joinAlias; 313 | } 314 | 315 | if ($referenceDirection == ':') { // back reference 316 | $primaryKey = $this->getStructure()->getPrimaryKey($mainTable); 317 | $foreignKey = $this->getStructure()->getForeignKey($mainTable); 318 | 319 | return " $clause $joinTable$asJoinAlias ON $joinAlias.$foreignKey = $mainTable.$primaryKey"; 320 | } else { 321 | $primaryKey = $this->getStructure()->getPrimaryKey($joinTable); 322 | $foreignKey = $this->getStructure()->getForeignKey($joinTable); 323 | 324 | return " $clause $joinTable$asJoinAlias ON $joinAlias.$primaryKey = $mainTable.$foreignKey"; 325 | } 326 | } 327 | 328 | /** 329 | * Create undefined joins from statement with column with referenced tables 330 | * 331 | * @param string $statement 332 | * 333 | * @return string - the rewritten $statement (e.g. tab1.tab2:col => tab2.col) 334 | */ 335 | private function createUndefinedJoins($statement) 336 | { 337 | if ($this->isEscapedJoin($statement)) { 338 | return $statement; 339 | } 340 | 341 | $separator = null; 342 | // if we're in here, this is a where clause 343 | if (is_array($statement)) { 344 | $separator = $statement[0]; 345 | $statement = $statement[1]; 346 | } 347 | 348 | // matches a table name made of any printable characters followed by a dot/colon, 349 | // followed by any letters, numbers and most punctuation (to exclude '*') 350 | $this->regex->tableJoinFull($statement, $matches); 351 | 352 | foreach ($matches[1] as $join) { 353 | // remove the trailing dot and compare with the joins we already have 354 | if (!in_array(substr($join, 0, -1), $this->joins)) { 355 | $this->addJoinStatements('LEFT JOIN', $join); 356 | } 357 | } 358 | 359 | // don't rewrite table from other databases 360 | foreach ($this->joins as $join) { 361 | if (strpos($join, '.') !== false && strpos($statement, $join) === 0) { 362 | // rebuild the where statement 363 | if ($separator !== null) { 364 | $statement = [$separator, $statement]; 365 | } 366 | 367 | return $statement; 368 | } 369 | } 370 | 371 | $statement = $this->regex->removeAdditionalJoins($statement); 372 | 373 | // rebuild the where statement 374 | if ($separator !== null) { 375 | $statement = [$separator, $statement]; 376 | } 377 | 378 | return $statement; 379 | } 380 | 381 | /** 382 | * @throws Exception 383 | * 384 | * @return string 385 | */ 386 | protected function buildQuery() 387 | { 388 | // first create extra join from statements with columns with referenced tables 389 | $statementsWithReferences = ['WHERE', 'SELECT', 'GROUP BY', 'ORDER BY']; 390 | 391 | foreach ($statementsWithReferences as $clause) { 392 | if (array_key_exists($clause, $this->statements)) { 393 | $this->statements[$clause] = array_map([$this, 'createUndefinedJoins'], $this->statements[$clause]); 394 | } 395 | } 396 | 397 | return parent::buildQuery(); 398 | } 399 | 400 | /** 401 | * @param $statement 402 | * 403 | * @return bool 404 | */ 405 | protected function isEscapedJoin($statement) 406 | { 407 | if (is_array($statement)) { 408 | $statement = $statement[1]; 409 | } 410 | 411 | return !$this->isSmartJoinEnabled || strpos($statement, '\.') !== false || strpos($statement, '\:') !== false; 412 | } 413 | 414 | /** 415 | * @param $statement 416 | * 417 | * @return array 418 | */ 419 | private function setJoinNameAlias($statement) 420 | { 421 | $this->regex->tableAlias($statement, $matches); // store any found alias in $matches 422 | $joinAlias = ''; 423 | $joinTable = ''; 424 | 425 | if ($matches) { 426 | $joinTable = $matches[1]; 427 | if (isset($matches[4]) && !in_array(strtoupper($matches[4]), ['ON', 'USING'])) { 428 | $joinAlias = $matches[4]; 429 | } 430 | } 431 | 432 | return [$joinAlias, $joinTable]; 433 | } 434 | 435 | /** 436 | * @param $table 437 | * @param $joinItem 438 | * 439 | * @return bool 440 | */ 441 | private function matchTableWithJoin($table, $joinItem) 442 | { 443 | return $table == substr($joinItem, 0, -1); 444 | } 445 | 446 | /** 447 | * @param $clause 448 | * @param $statement 449 | * @param $parameters 450 | * @param $joinAlias 451 | * @param $joinTable 452 | * 453 | * @return $this 454 | */ 455 | private function addRawJoins($clause, $statement, $parameters, $joinAlias, $joinTable) 456 | { 457 | if (!$joinAlias) { 458 | $joinAlias = $joinTable; 459 | } 460 | 461 | if (in_array($joinAlias, $this->joins)) { 462 | return $this; 463 | } else { 464 | $this->joins[] = $joinAlias; 465 | $statement = " $clause $statement"; 466 | 467 | return $this->addStatement('JOIN', $statement, $parameters); 468 | } 469 | } 470 | 471 | /** 472 | * @return string 473 | */ 474 | private function setMainTable() 475 | { 476 | if (isset($this->statements['FROM'])) { 477 | return $this->statements['FROM']; 478 | } elseif (isset($this->statements['UPDATE'])) { 479 | return $this->statements['UPDATE']; 480 | } 481 | 482 | return ''; 483 | } 484 | 485 | /** 486 | * @param $clause 487 | * @param $parameters 488 | * @param $mainTable 489 | * @param $joinItem 490 | * @param $lastItem 491 | * @param $joinAlias 492 | * 493 | * @return mixed 494 | */ 495 | private function applyTableJoin($clause, $parameters, $mainTable, $joinItem, $lastItem, $joinAlias) 496 | { 497 | $alias = ''; 498 | 499 | if ($joinItem == $lastItem) { 500 | $alias = $joinAlias; // use $joinAlias only for $lastItem 501 | } 502 | 503 | $newJoin = $this->createJoinStatement($clause, $mainTable, $joinItem, $alias); 504 | 505 | if ($newJoin) { 506 | $this->addStatement('JOIN', $newJoin, $parameters); 507 | } 508 | 509 | return $joinItem; 510 | } 511 | 512 | public function __clone() 513 | { 514 | foreach ($this->clauses as $clause => $value) { 515 | if (is_array($value) && $value[0] instanceof Common) { 516 | $this->clauses[$clause][0] = $this; 517 | } 518 | } 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /src/Queries/Base.php: -------------------------------------------------------------------------------- 1 | currentFetchMode = defined('PDO::FETCH_DEFAULT') ? PDO::FETCH_DEFAULT : PDO::FETCH_BOTH; 54 | $this->fluent = $fluent; 55 | $this->clauses = $clauses; 56 | $this->result = null; 57 | 58 | $this->initClauses(); 59 | 60 | $this->regex = new Regex(); 61 | } 62 | 63 | /** 64 | * Return formatted query when request class representation 65 | * ie: echo $query 66 | * 67 | * @return string - formatted query 68 | * 69 | * @throws Exception 70 | */ 71 | public function __toString() 72 | { 73 | return $this->getQuery(); 74 | } 75 | 76 | /** 77 | * Initialize statement and parameter clauses. 78 | */ 79 | private function initClauses(): void 80 | { 81 | foreach ($this->clauses as $clause => $value) { 82 | if ($value) { 83 | $this->statements[$clause] = []; 84 | $this->parameters[$clause] = []; 85 | } else { 86 | $this->statements[$clause] = null; 87 | $this->parameters[$clause] = null; 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Add statement for all clauses except WHERE 94 | * 95 | * @param $clause 96 | * @param $statement 97 | * @param array $parameters 98 | * 99 | * @return $this 100 | */ 101 | protected function addStatement($clause, $statement, $parameters = []) 102 | { 103 | if ($statement === null) { 104 | return $this->resetClause($clause); 105 | } 106 | 107 | if ($this->clauses[$clause]) { 108 | if (is_array($statement)) { 109 | $this->statements[$clause] = array_merge($this->statements[$clause], $statement); 110 | } else { 111 | $this->statements[$clause][] = $statement; 112 | } 113 | 114 | $this->parameters[$clause] = array_merge($this->parameters[$clause], $parameters); 115 | } else { 116 | $this->statements[$clause] = $statement; 117 | $this->parameters[$clause] = $parameters; 118 | } 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Add statement for all kind of clauses 125 | * 126 | * @param $statement 127 | * @param string $separator - should be AND or OR 128 | * @param array $parameters 129 | * 130 | * @return $this 131 | */ 132 | protected function addWhereStatement($statement, string $separator = 'AND', $parameters = []) 133 | { 134 | if ($statement === null) { 135 | return $this->resetClause('WHERE'); 136 | } 137 | 138 | if (is_array($statement)) { 139 | foreach ($statement as $s) { 140 | $this->statements['WHERE'][] = [$separator, $s]; 141 | } 142 | } else { 143 | $this->statements['WHERE'][] = [$separator, $statement]; 144 | } 145 | 146 | $this->parameters['WHERE'] = array_merge($this->parameters['WHERE'], $parameters); 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Remove all prev defined statements 153 | * 154 | * @param $clause 155 | * 156 | * @return $this 157 | */ 158 | protected function resetClause($clause) 159 | { 160 | $this->statements[$clause] = null; 161 | $this->parameters[$clause] = []; 162 | if (isset($this->clauses[$clause]) && $this->clauses[$clause]) { 163 | $this->statements[$clause] = []; 164 | } 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Implements method from IteratorAggregate 171 | * 172 | * @return PDOStatement 173 | * 174 | * @throws Exception 175 | */ 176 | #[\ReturnTypeWillChange] 177 | public function getIterator() 178 | { 179 | return $this->execute(); 180 | } 181 | 182 | /** 183 | * Execute query with earlier added parameters 184 | * 185 | * @return PDOStatement 186 | * 187 | * @throws Exception 188 | */ 189 | public function execute() 190 | { 191 | $startTime = microtime(true); 192 | 193 | $query = $this->buildQuery(); 194 | $parameters = $this->buildParameters(); 195 | 196 | $this->prepareQuery($query); 197 | 198 | if ($this->result !== false) { 199 | $this->setObjectFetchMode($this->result); 200 | 201 | $execTime = microtime(true); 202 | 203 | $this->executeQuery($parameters, $startTime, $execTime); 204 | $this->debug(); 205 | } 206 | 207 | return $this->result; 208 | } 209 | 210 | /** 211 | * @return Structure 212 | */ 213 | protected function getStructure(): Structure 214 | { 215 | return $this->fluent->getStructure(); 216 | } 217 | 218 | /** 219 | * Get PDOStatement result 220 | * 221 | * @return PDOStatement|null|bool 222 | */ 223 | public function getResult() 224 | { 225 | return $this->result; 226 | } 227 | 228 | /** 229 | * Get query parameters 230 | * 231 | * @return array 232 | */ 233 | public function getParameters(): array 234 | { 235 | return $this->buildParameters(); 236 | } 237 | 238 | /** 239 | * @return array 240 | */ 241 | public function getRawClauses(): array 242 | { 243 | return $this->clauses; 244 | } 245 | 246 | /** 247 | * @return array 248 | */ 249 | public function getRawStatements(): array 250 | { 251 | return $this->statements; 252 | } 253 | 254 | /** 255 | * @return array 256 | */ 257 | public function getRawParameters(): array 258 | { 259 | return $this->parameters; 260 | } 261 | 262 | /** 263 | * Gets the total time of query building, preparation and execution 264 | * 265 | * @return float 266 | */ 267 | public function getTotalTime(): float 268 | { 269 | return $this->totalTime; 270 | } 271 | 272 | /** 273 | * Gets the query execution time 274 | * 275 | * @return float 276 | */ 277 | public function getExecutionTime(): float 278 | { 279 | return $this->executionTime; 280 | } 281 | 282 | /** 283 | * @return string 284 | */ 285 | public function getMessage(): string 286 | { 287 | return $this->message; 288 | } 289 | 290 | /** 291 | * Get query string 292 | * 293 | * @param bool $formatted - Return formatted query 294 | * 295 | * @return string 296 | * @throws Exception 297 | */ 298 | public function getQuery(bool $formatted = true): string 299 | { 300 | $query = $this->buildQuery(); 301 | 302 | if ($formatted) { 303 | $query = Utilities::formatQuery($query); 304 | } 305 | 306 | return $query; 307 | } 308 | 309 | /** 310 | * Select an item as object 311 | * 312 | * @param object|boolean $object If set to true, items are returned as stdClass, otherwise a class 313 | * name can be passed and a new instance of this class is returned. 314 | * Can be set to false to return items as an associative array. 315 | * 316 | * @return $this 317 | */ 318 | public function asObject($object = true) 319 | { 320 | $this->object = $object; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * Converts php null values to Literal instances to be inserted into a database 327 | */ 328 | protected function convertNullValues(): void 329 | { 330 | $filterList = ['VALUES', 'ON DUPLICATE KEY UPDATE', 'SET']; 331 | 332 | foreach ($this->statements as $clause => $statement) { 333 | if (in_array($clause, $filterList)) { 334 | if (isset($statement[0])) { 335 | for ($i = 0, $iMax = count($statement); $i < $iMax; $i++) { 336 | foreach ($statement[$i] as $key => $value) { 337 | $this->statements[$clause][$i][$key] = Utilities::nullToLiteral($value); 338 | } 339 | } 340 | } else { 341 | foreach ($statement as $key => $value) { 342 | $this->statements[$clause][$key] = Utilities::nullToLiteral($value); 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | /** 350 | * Generate query 351 | * 352 | * @return string 353 | * @throws Exception 354 | */ 355 | protected function buildQuery() 356 | { 357 | if ($this->fluent->convertWrite === true) { 358 | $this->convertNullValues(); 359 | } 360 | 361 | $query = ''; 362 | 363 | foreach ($this->clauses as $clause => $separator) { 364 | if ($this->clauseNotEmpty($clause)) { 365 | if (is_string($separator)) { 366 | $query .= " {$clause} " . implode($separator, $this->statements[$clause]); 367 | } elseif ($separator === null) { 368 | $query .= " {$clause} {$this->statements[$clause]}"; 369 | } elseif (is_callable($separator)) { 370 | $query .= $separator(); 371 | } else { 372 | throw new Exception("Clause '$clause' is incorrectly set to '$separator'."); 373 | } 374 | } 375 | } 376 | 377 | return trim(str_replace(['\.', '\:'], ['.', ':'], $query)); 378 | } 379 | 380 | /** 381 | * @return array 382 | */ 383 | protected function buildParameters(): array 384 | { 385 | $parameters = []; 386 | foreach ($this->parameters as $clauses) { 387 | if ($this->fluent->convertWrite === true) { 388 | $clauses = Utilities::convertSqlWriteValues($clauses); 389 | } 390 | 391 | if (is_array($clauses)) { 392 | foreach ($clauses as $key => $value) { 393 | if (strpos($key, ':') === 0) { // these are named params e.g. (':name' => 'Mark') 394 | $parameters += [$key => $value]; 395 | } else { 396 | $parameters[] = $value; 397 | } 398 | } 399 | } elseif ($clauses !== false && $clauses !== null) { 400 | $parameters[] = $clauses; 401 | } 402 | } 403 | 404 | return $parameters; 405 | } 406 | 407 | /** 408 | * @param $value 409 | * 410 | * @return string 411 | */ 412 | protected function quote($value) 413 | { 414 | if (!isset($value)) { 415 | return "NULL"; 416 | } 417 | 418 | if (is_array($value)) { // (a, b) IN ((1, 2), (3, 4)) 419 | return "(" . implode(", ", array_map([$this, 'quote'], $value)) . ")"; 420 | } 421 | 422 | $value = $this->formatValue($value); 423 | if (is_float($value)) { 424 | return sprintf("%F", $value); // otherwise depends on setlocale() 425 | } 426 | 427 | if ($value === true) { 428 | return 1; 429 | } 430 | 431 | if ($value === false) { 432 | return 0; 433 | } 434 | 435 | if (is_int($value) || $value instanceof Literal) { // number or SQL code - for example "NOW()" 436 | return (string)$value; 437 | } 438 | 439 | return $this->fluent->getPdo()->quote($value); 440 | } 441 | 442 | /** 443 | * @param $clause 444 | * 445 | * @return bool 446 | */ 447 | private function clauseNotEmpty($clause) 448 | { 449 | if ((Utilities::isCountable($this->statements[$clause])) && $this->clauses[$clause]) { 450 | return (bool)count($this->statements[$clause]); 451 | } 452 | 453 | return (bool)$this->statements[$clause]; 454 | } 455 | 456 | /** 457 | * @param \DateTime $val 458 | * 459 | * @return mixed 460 | */ 461 | private function formatValue($val) 462 | { 463 | if ($val instanceof DateTime) { 464 | return $val->format('Y-m-d H:i:s'); // may be driver specific 465 | } 466 | 467 | return $val; 468 | } 469 | 470 | /** 471 | * @param string $query 472 | * 473 | * @throws Exception 474 | */ 475 | private function prepareQuery($query): void 476 | { 477 | $this->result = $this->fluent->getPdo()->prepare($query); 478 | 479 | /* 480 | At this point, $result is a PDOStatement instance, or false. 481 | PDO::prepare() does not reliably return errors. Some database drivers 482 | do not support prepared statements, and PHP emulates them. Postgresql 483 | does support prepared statements, but PHP does not call Postgresql's 484 | prepare function until we call PDOStatement::execute() below. 485 | If PDO::prepare() was consistent, this is where we would check 486 | for prepare errors, such as invalid SQL. 487 | */ 488 | 489 | if ($this->result === false) { 490 | $error = $this->fluent->getPdo()->errorInfo(); 491 | $this->message = "SQLSTATE: {$error[0]} - Driver Code: {$error[1]} - Message: {$error[2]}"; 492 | 493 | if ($this->fluent->exceptionOnError === true) { 494 | throw new Exception($this->message); 495 | } 496 | } 497 | } 498 | 499 | /** 500 | * @param array $parameters 501 | * @param int $startTime 502 | * @param int $execTime 503 | * 504 | * @throws Exception 505 | */ 506 | private function executeQuery($parameters, $startTime, $execTime): void 507 | { 508 | if ($this->result->execute($parameters) === true) { 509 | $this->executionTime = microtime(true) - $execTime; 510 | $this->totalTime = microtime(true) - $startTime; 511 | } else { 512 | $error = $this->result->errorInfo(); 513 | $this->message = "SQLSTATE: {$error[0]} - Driver Code: {$error[1]} - Message: {$error[2]}"; 514 | 515 | if ($this->fluent->exceptionOnError === true) { 516 | throw new Exception($this->message); 517 | } 518 | 519 | $this->result = false; 520 | } 521 | } 522 | 523 | /** 524 | * @param PDOStatement $result 525 | */ 526 | private function setObjectFetchMode(PDOStatement $result): void 527 | { 528 | if ($this->object !== false) { 529 | if (class_exists($this->object)) { 530 | $this->currentFetchMode = PDO::FETCH_CLASS; 531 | $result->setFetchMode($this->currentFetchMode, $this->object); 532 | } else { 533 | $this->currentFetchMode = PDO::FETCH_OBJ; 534 | $result->setFetchMode($this->currentFetchMode); 535 | } 536 | } elseif ($this->fluent->getPdo()->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE) === PDO::FETCH_BOTH) { 537 | $this->currentFetchMode = PDO::FETCH_ASSOC; 538 | $result->setFetchMode($this->currentFetchMode); 539 | } 540 | } 541 | 542 | /** 543 | * Echo/pass a debug string 544 | * 545 | * @throws Exception 546 | */ 547 | private function debug() 548 | { 549 | if (!empty($this->fluent->debug)) { 550 | if (!is_callable($this->fluent->debug)) { 551 | $backtrace = ''; 552 | $query = $this->getQuery(); 553 | $parameters = $this->getParameters(); 554 | $debug = ''; 555 | 556 | if ($parameters) { 557 | $debug = '# parameters: ' . implode(', ', array_map([$this, 'quote'], $parameters)) . "\n"; 558 | } 559 | 560 | $debug .= $query; 561 | 562 | foreach (debug_backtrace() as $backtrace) { 563 | if (isset($backtrace['file']) && !$this->regex->compareLocation($backtrace['file'])) { 564 | // stop at the first file outside the FluentPDO source 565 | break; 566 | } 567 | } 568 | 569 | $time = sprintf('%0.3f', $this->totalTime * 1000) . 'ms'; 570 | $rows = ($this->result) ? $this->result->rowCount() : 0; 571 | $finalString = "# {$backtrace['file']}:{$backtrace['line']} ({$time}; rows = {$rows})\n{$debug}\n\n"; 572 | 573 | // if STDERR is set, send there, otherwise just output the string 574 | if (defined('STDERR') && is_resource(STDERR)) { 575 | fwrite(STDERR, $finalString); 576 | } else { 577 | echo $finalString; 578 | } 579 | } else { 580 | $debug = $this->fluent->debug; 581 | $debug($this); 582 | } 583 | } 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "43294b3845978b000b9d0d3002f2c3d3", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.3.1", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", 21 | "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^6.0", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^0.13", 32 | "phpstan/phpstan-phpunit": "^0.11", 33 | "phpstan/phpstan-shim": "^0.11", 34 | "phpunit/phpunit": "^7.0" 35 | }, 36 | "type": "library", 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "1.2.x-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Marco Pivetta", 54 | "email": "ocramius@gmail.com", 55 | "homepage": "http://ocramius.github.com/" 56 | } 57 | ], 58 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 59 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 60 | "keywords": [ 61 | "constructor", 62 | "instantiate" 63 | ], 64 | "funding": [ 65 | { 66 | "url": "https://www.doctrine-project.org/sponsorship.html", 67 | "type": "custom" 68 | }, 69 | { 70 | "url": "https://www.patreon.com/phpdoctrine", 71 | "type": "patreon" 72 | }, 73 | { 74 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 75 | "type": "tidelift" 76 | } 77 | ], 78 | "time": "2020-05-29T17:27:14+00:00" 79 | }, 80 | { 81 | "name": "envms/fluent-test", 82 | "version": "v1.0.1", 83 | "source": { 84 | "type": "git", 85 | "url": "https://github.com/envms/fluent-test.git", 86 | "reference": "2db2a96ce65f64b7c6328040b7da5d2240782f23" 87 | }, 88 | "dist": { 89 | "type": "zip", 90 | "url": "https://api.github.com/repos/envms/fluent-test/zipball/2db2a96ce65f64b7c6328040b7da5d2240782f23", 91 | "reference": "2db2a96ce65f64b7c6328040b7da5d2240782f23", 92 | "shasum": "" 93 | }, 94 | "type": "library", 95 | "autoload": { 96 | "psr-4": { 97 | "Envms\\FluentTest\\": "src/" 98 | } 99 | }, 100 | "notification-url": "https://packagist.org/downloads/", 101 | "license": [ 102 | "Apache-2.0", 103 | "GPL-2.0+" 104 | ], 105 | "authors": [ 106 | { 107 | "name": "envms", 108 | "homepage": "http://env.ms" 109 | } 110 | ], 111 | "description": "A library for testing FluentPDO with mock data and classes", 112 | "homepage": "https://github.com/envms/fluent-test", 113 | "keywords": [ 114 | "fluentpdo", 115 | "library", 116 | "test" 117 | ], 118 | "time": "2018-08-23T17:42:04+00:00" 119 | }, 120 | { 121 | "name": "myclabs/deep-copy", 122 | "version": "1.10.1", 123 | "source": { 124 | "type": "git", 125 | "url": "https://github.com/myclabs/DeepCopy.git", 126 | "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" 127 | }, 128 | "dist": { 129 | "type": "zip", 130 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", 131 | "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", 132 | "shasum": "" 133 | }, 134 | "require": { 135 | "php": "^7.1 || ^8.0" 136 | }, 137 | "replace": { 138 | "myclabs/deep-copy": "self.version" 139 | }, 140 | "require-dev": { 141 | "doctrine/collections": "^1.0", 142 | "doctrine/common": "^2.6", 143 | "phpunit/phpunit": "^7.1" 144 | }, 145 | "type": "library", 146 | "autoload": { 147 | "psr-4": { 148 | "DeepCopy\\": "src/DeepCopy/" 149 | }, 150 | "files": [ 151 | "src/DeepCopy/deep_copy.php" 152 | ] 153 | }, 154 | "notification-url": "https://packagist.org/downloads/", 155 | "license": [ 156 | "MIT" 157 | ], 158 | "description": "Create deep copies (clones) of your objects", 159 | "keywords": [ 160 | "clone", 161 | "copy", 162 | "duplicate", 163 | "object", 164 | "object graph" 165 | ], 166 | "funding": [ 167 | { 168 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 169 | "type": "tidelift" 170 | } 171 | ], 172 | "time": "2020-06-29T13:22:24+00:00" 173 | }, 174 | { 175 | "name": "phar-io/manifest", 176 | "version": "1.0.3", 177 | "source": { 178 | "type": "git", 179 | "url": "https://github.com/phar-io/manifest.git", 180 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 181 | }, 182 | "dist": { 183 | "type": "zip", 184 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 185 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 186 | "shasum": "" 187 | }, 188 | "require": { 189 | "ext-dom": "*", 190 | "ext-phar": "*", 191 | "phar-io/version": "^2.0", 192 | "php": "^5.6 || ^7.0" 193 | }, 194 | "type": "library", 195 | "extra": { 196 | "branch-alias": { 197 | "dev-master": "1.0.x-dev" 198 | } 199 | }, 200 | "autoload": { 201 | "classmap": [ 202 | "src/" 203 | ] 204 | }, 205 | "notification-url": "https://packagist.org/downloads/", 206 | "license": [ 207 | "BSD-3-Clause" 208 | ], 209 | "authors": [ 210 | { 211 | "name": "Arne Blankerts", 212 | "email": "arne@blankerts.de", 213 | "role": "Developer" 214 | }, 215 | { 216 | "name": "Sebastian Heuer", 217 | "email": "sebastian@phpeople.de", 218 | "role": "Developer" 219 | }, 220 | { 221 | "name": "Sebastian Bergmann", 222 | "email": "sebastian@phpunit.de", 223 | "role": "Developer" 224 | } 225 | ], 226 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 227 | "time": "2018-07-08T19:23:20+00:00" 228 | }, 229 | { 230 | "name": "phar-io/version", 231 | "version": "2.0.1", 232 | "source": { 233 | "type": "git", 234 | "url": "https://github.com/phar-io/version.git", 235 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 236 | }, 237 | "dist": { 238 | "type": "zip", 239 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 240 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 241 | "shasum": "" 242 | }, 243 | "require": { 244 | "php": "^5.6 || ^7.0" 245 | }, 246 | "type": "library", 247 | "autoload": { 248 | "classmap": [ 249 | "src/" 250 | ] 251 | }, 252 | "notification-url": "https://packagist.org/downloads/", 253 | "license": [ 254 | "BSD-3-Clause" 255 | ], 256 | "authors": [ 257 | { 258 | "name": "Arne Blankerts", 259 | "email": "arne@blankerts.de", 260 | "role": "Developer" 261 | }, 262 | { 263 | "name": "Sebastian Heuer", 264 | "email": "sebastian@phpeople.de", 265 | "role": "Developer" 266 | }, 267 | { 268 | "name": "Sebastian Bergmann", 269 | "email": "sebastian@phpunit.de", 270 | "role": "Developer" 271 | } 272 | ], 273 | "description": "Library for handling version information and constraints", 274 | "time": "2018-07-08T19:19:57+00:00" 275 | }, 276 | { 277 | "name": "phpdocumentor/reflection-common", 278 | "version": "2.2.0", 279 | "source": { 280 | "type": "git", 281 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 282 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" 283 | }, 284 | "dist": { 285 | "type": "zip", 286 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", 287 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", 288 | "shasum": "" 289 | }, 290 | "require": { 291 | "php": "^7.2 || ^8.0" 292 | }, 293 | "type": "library", 294 | "extra": { 295 | "branch-alias": { 296 | "dev-2.x": "2.x-dev" 297 | } 298 | }, 299 | "autoload": { 300 | "psr-4": { 301 | "phpDocumentor\\Reflection\\": "src/" 302 | } 303 | }, 304 | "notification-url": "https://packagist.org/downloads/", 305 | "license": [ 306 | "MIT" 307 | ], 308 | "authors": [ 309 | { 310 | "name": "Jaap van Otterdijk", 311 | "email": "opensource@ijaap.nl" 312 | } 313 | ], 314 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 315 | "homepage": "http://www.phpdoc.org", 316 | "keywords": [ 317 | "FQSEN", 318 | "phpDocumentor", 319 | "phpdoc", 320 | "reflection", 321 | "static analysis" 322 | ], 323 | "time": "2020-06-27T09:03:43+00:00" 324 | }, 325 | { 326 | "name": "phpdocumentor/reflection-docblock", 327 | "version": "5.2.1", 328 | "source": { 329 | "type": "git", 330 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 331 | "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" 332 | }, 333 | "dist": { 334 | "type": "zip", 335 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", 336 | "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", 337 | "shasum": "" 338 | }, 339 | "require": { 340 | "ext-filter": "*", 341 | "php": "^7.2 || ^8.0", 342 | "phpdocumentor/reflection-common": "^2.2", 343 | "phpdocumentor/type-resolver": "^1.3", 344 | "webmozart/assert": "^1.9.1" 345 | }, 346 | "require-dev": { 347 | "mockery/mockery": "~1.3.2" 348 | }, 349 | "type": "library", 350 | "extra": { 351 | "branch-alias": { 352 | "dev-master": "5.x-dev" 353 | } 354 | }, 355 | "autoload": { 356 | "psr-4": { 357 | "phpDocumentor\\Reflection\\": "src" 358 | } 359 | }, 360 | "notification-url": "https://packagist.org/downloads/", 361 | "license": [ 362 | "MIT" 363 | ], 364 | "authors": [ 365 | { 366 | "name": "Mike van Riel", 367 | "email": "me@mikevanriel.com" 368 | }, 369 | { 370 | "name": "Jaap van Otterdijk", 371 | "email": "account@ijaap.nl" 372 | } 373 | ], 374 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 375 | "time": "2020-08-15T11:14:08+00:00" 376 | }, 377 | { 378 | "name": "phpdocumentor/type-resolver", 379 | "version": "1.3.0", 380 | "source": { 381 | "type": "git", 382 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 383 | "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" 384 | }, 385 | "dist": { 386 | "type": "zip", 387 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", 388 | "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", 389 | "shasum": "" 390 | }, 391 | "require": { 392 | "php": "^7.2 || ^8.0", 393 | "phpdocumentor/reflection-common": "^2.0" 394 | }, 395 | "require-dev": { 396 | "ext-tokenizer": "*" 397 | }, 398 | "type": "library", 399 | "extra": { 400 | "branch-alias": { 401 | "dev-1.x": "1.x-dev" 402 | } 403 | }, 404 | "autoload": { 405 | "psr-4": { 406 | "phpDocumentor\\Reflection\\": "src" 407 | } 408 | }, 409 | "notification-url": "https://packagist.org/downloads/", 410 | "license": [ 411 | "MIT" 412 | ], 413 | "authors": [ 414 | { 415 | "name": "Mike van Riel", 416 | "email": "me@mikevanriel.com" 417 | } 418 | ], 419 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 420 | "time": "2020-06-27T10:12:23+00:00" 421 | }, 422 | { 423 | "name": "phpspec/prophecy", 424 | "version": "1.11.1", 425 | "source": { 426 | "type": "git", 427 | "url": "https://github.com/phpspec/prophecy.git", 428 | "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" 429 | }, 430 | "dist": { 431 | "type": "zip", 432 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", 433 | "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", 434 | "shasum": "" 435 | }, 436 | "require": { 437 | "doctrine/instantiator": "^1.2", 438 | "php": "^7.2", 439 | "phpdocumentor/reflection-docblock": "^5.0", 440 | "sebastian/comparator": "^3.0 || ^4.0", 441 | "sebastian/recursion-context": "^3.0 || ^4.0" 442 | }, 443 | "require-dev": { 444 | "phpspec/phpspec": "^6.0", 445 | "phpunit/phpunit": "^8.0" 446 | }, 447 | "type": "library", 448 | "extra": { 449 | "branch-alias": { 450 | "dev-master": "1.11.x-dev" 451 | } 452 | }, 453 | "autoload": { 454 | "psr-4": { 455 | "Prophecy\\": "src/Prophecy" 456 | } 457 | }, 458 | "notification-url": "https://packagist.org/downloads/", 459 | "license": [ 460 | "MIT" 461 | ], 462 | "authors": [ 463 | { 464 | "name": "Konstantin Kudryashov", 465 | "email": "ever.zet@gmail.com", 466 | "homepage": "http://everzet.com" 467 | }, 468 | { 469 | "name": "Marcello Duarte", 470 | "email": "marcello.duarte@gmail.com" 471 | } 472 | ], 473 | "description": "Highly opinionated mocking framework for PHP 5.3+", 474 | "homepage": "https://github.com/phpspec/prophecy", 475 | "keywords": [ 476 | "Double", 477 | "Dummy", 478 | "fake", 479 | "mock", 480 | "spy", 481 | "stub" 482 | ], 483 | "time": "2020-07-08T12:44:21+00:00" 484 | }, 485 | { 486 | "name": "phpunit/php-code-coverage", 487 | "version": "7.0.10", 488 | "source": { 489 | "type": "git", 490 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 491 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" 492 | }, 493 | "dist": { 494 | "type": "zip", 495 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", 496 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", 497 | "shasum": "" 498 | }, 499 | "require": { 500 | "ext-dom": "*", 501 | "ext-xmlwriter": "*", 502 | "php": "^7.2", 503 | "phpunit/php-file-iterator": "^2.0.2", 504 | "phpunit/php-text-template": "^1.2.1", 505 | "phpunit/php-token-stream": "^3.1.1", 506 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 507 | "sebastian/environment": "^4.2.2", 508 | "sebastian/version": "^2.0.1", 509 | "theseer/tokenizer": "^1.1.3" 510 | }, 511 | "require-dev": { 512 | "phpunit/phpunit": "^8.2.2" 513 | }, 514 | "suggest": { 515 | "ext-xdebug": "^2.7.2" 516 | }, 517 | "type": "library", 518 | "extra": { 519 | "branch-alias": { 520 | "dev-master": "7.0-dev" 521 | } 522 | }, 523 | "autoload": { 524 | "classmap": [ 525 | "src/" 526 | ] 527 | }, 528 | "notification-url": "https://packagist.org/downloads/", 529 | "license": [ 530 | "BSD-3-Clause" 531 | ], 532 | "authors": [ 533 | { 534 | "name": "Sebastian Bergmann", 535 | "email": "sebastian@phpunit.de", 536 | "role": "lead" 537 | } 538 | ], 539 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 540 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 541 | "keywords": [ 542 | "coverage", 543 | "testing", 544 | "xunit" 545 | ], 546 | "time": "2019-11-20T13:55:58+00:00" 547 | }, 548 | { 549 | "name": "phpunit/php-file-iterator", 550 | "version": "2.0.2", 551 | "source": { 552 | "type": "git", 553 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 554 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 555 | }, 556 | "dist": { 557 | "type": "zip", 558 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 559 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 560 | "shasum": "" 561 | }, 562 | "require": { 563 | "php": "^7.1" 564 | }, 565 | "require-dev": { 566 | "phpunit/phpunit": "^7.1" 567 | }, 568 | "type": "library", 569 | "extra": { 570 | "branch-alias": { 571 | "dev-master": "2.0.x-dev" 572 | } 573 | }, 574 | "autoload": { 575 | "classmap": [ 576 | "src/" 577 | ] 578 | }, 579 | "notification-url": "https://packagist.org/downloads/", 580 | "license": [ 581 | "BSD-3-Clause" 582 | ], 583 | "authors": [ 584 | { 585 | "name": "Sebastian Bergmann", 586 | "email": "sebastian@phpunit.de", 587 | "role": "lead" 588 | } 589 | ], 590 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 591 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 592 | "keywords": [ 593 | "filesystem", 594 | "iterator" 595 | ], 596 | "time": "2018-09-13T20:33:42+00:00" 597 | }, 598 | { 599 | "name": "phpunit/php-text-template", 600 | "version": "1.2.1", 601 | "source": { 602 | "type": "git", 603 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 604 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 605 | }, 606 | "dist": { 607 | "type": "zip", 608 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 609 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 610 | "shasum": "" 611 | }, 612 | "require": { 613 | "php": ">=5.3.3" 614 | }, 615 | "type": "library", 616 | "autoload": { 617 | "classmap": [ 618 | "src/" 619 | ] 620 | }, 621 | "notification-url": "https://packagist.org/downloads/", 622 | "license": [ 623 | "BSD-3-Clause" 624 | ], 625 | "authors": [ 626 | { 627 | "name": "Sebastian Bergmann", 628 | "email": "sebastian@phpunit.de", 629 | "role": "lead" 630 | } 631 | ], 632 | "description": "Simple template engine.", 633 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 634 | "keywords": [ 635 | "template" 636 | ], 637 | "time": "2015-06-21T13:50:34+00:00" 638 | }, 639 | { 640 | "name": "phpunit/php-timer", 641 | "version": "2.1.2", 642 | "source": { 643 | "type": "git", 644 | "url": "https://github.com/sebastianbergmann/php-timer.git", 645 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" 646 | }, 647 | "dist": { 648 | "type": "zip", 649 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", 650 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", 651 | "shasum": "" 652 | }, 653 | "require": { 654 | "php": "^7.1" 655 | }, 656 | "require-dev": { 657 | "phpunit/phpunit": "^7.0" 658 | }, 659 | "type": "library", 660 | "extra": { 661 | "branch-alias": { 662 | "dev-master": "2.1-dev" 663 | } 664 | }, 665 | "autoload": { 666 | "classmap": [ 667 | "src/" 668 | ] 669 | }, 670 | "notification-url": "https://packagist.org/downloads/", 671 | "license": [ 672 | "BSD-3-Clause" 673 | ], 674 | "authors": [ 675 | { 676 | "name": "Sebastian Bergmann", 677 | "email": "sebastian@phpunit.de", 678 | "role": "lead" 679 | } 680 | ], 681 | "description": "Utility class for timing", 682 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 683 | "keywords": [ 684 | "timer" 685 | ], 686 | "time": "2019-06-07T04:22:29+00:00" 687 | }, 688 | { 689 | "name": "phpunit/php-token-stream", 690 | "version": "3.1.1", 691 | "source": { 692 | "type": "git", 693 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 694 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" 695 | }, 696 | "dist": { 697 | "type": "zip", 698 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", 699 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", 700 | "shasum": "" 701 | }, 702 | "require": { 703 | "ext-tokenizer": "*", 704 | "php": "^7.1" 705 | }, 706 | "require-dev": { 707 | "phpunit/phpunit": "^7.0" 708 | }, 709 | "type": "library", 710 | "extra": { 711 | "branch-alias": { 712 | "dev-master": "3.1-dev" 713 | } 714 | }, 715 | "autoload": { 716 | "classmap": [ 717 | "src/" 718 | ] 719 | }, 720 | "notification-url": "https://packagist.org/downloads/", 721 | "license": [ 722 | "BSD-3-Clause" 723 | ], 724 | "authors": [ 725 | { 726 | "name": "Sebastian Bergmann", 727 | "email": "sebastian@phpunit.de" 728 | } 729 | ], 730 | "description": "Wrapper around PHP's tokenizer extension.", 731 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 732 | "keywords": [ 733 | "tokenizer" 734 | ], 735 | "abandoned": true, 736 | "time": "2019-09-17T06:23:10+00:00" 737 | }, 738 | { 739 | "name": "phpunit/phpunit", 740 | "version": "8.5.8", 741 | "source": { 742 | "type": "git", 743 | "url": "https://github.com/sebastianbergmann/phpunit.git", 744 | "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" 745 | }, 746 | "dist": { 747 | "type": "zip", 748 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", 749 | "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", 750 | "shasum": "" 751 | }, 752 | "require": { 753 | "doctrine/instantiator": "^1.2.0", 754 | "ext-dom": "*", 755 | "ext-json": "*", 756 | "ext-libxml": "*", 757 | "ext-mbstring": "*", 758 | "ext-xml": "*", 759 | "ext-xmlwriter": "*", 760 | "myclabs/deep-copy": "^1.9.1", 761 | "phar-io/manifest": "^1.0.3", 762 | "phar-io/version": "^2.0.1", 763 | "php": "^7.2", 764 | "phpspec/prophecy": "^1.8.1", 765 | "phpunit/php-code-coverage": "^7.0.7", 766 | "phpunit/php-file-iterator": "^2.0.2", 767 | "phpunit/php-text-template": "^1.2.1", 768 | "phpunit/php-timer": "^2.1.2", 769 | "sebastian/comparator": "^3.0.2", 770 | "sebastian/diff": "^3.0.2", 771 | "sebastian/environment": "^4.2.2", 772 | "sebastian/exporter": "^3.1.1", 773 | "sebastian/global-state": "^3.0.0", 774 | "sebastian/object-enumerator": "^3.0.3", 775 | "sebastian/resource-operations": "^2.0.1", 776 | "sebastian/type": "^1.1.3", 777 | "sebastian/version": "^2.0.1" 778 | }, 779 | "require-dev": { 780 | "ext-pdo": "*" 781 | }, 782 | "suggest": { 783 | "ext-soap": "*", 784 | "ext-xdebug": "*", 785 | "phpunit/php-invoker": "^2.0.0" 786 | }, 787 | "bin": [ 788 | "phpunit" 789 | ], 790 | "type": "library", 791 | "extra": { 792 | "branch-alias": { 793 | "dev-master": "8.5-dev" 794 | } 795 | }, 796 | "autoload": { 797 | "classmap": [ 798 | "src/" 799 | ] 800 | }, 801 | "notification-url": "https://packagist.org/downloads/", 802 | "license": [ 803 | "BSD-3-Clause" 804 | ], 805 | "authors": [ 806 | { 807 | "name": "Sebastian Bergmann", 808 | "email": "sebastian@phpunit.de", 809 | "role": "lead" 810 | } 811 | ], 812 | "description": "The PHP Unit Testing framework.", 813 | "homepage": "https://phpunit.de/", 814 | "keywords": [ 815 | "phpunit", 816 | "testing", 817 | "xunit" 818 | ], 819 | "funding": [ 820 | { 821 | "url": "https://phpunit.de/donate.html", 822 | "type": "custom" 823 | }, 824 | { 825 | "url": "https://github.com/sebastianbergmann", 826 | "type": "github" 827 | } 828 | ], 829 | "time": "2020-06-22T07:06:58+00:00" 830 | }, 831 | { 832 | "name": "sebastian/code-unit-reverse-lookup", 833 | "version": "1.0.1", 834 | "source": { 835 | "type": "git", 836 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 837 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 838 | }, 839 | "dist": { 840 | "type": "zip", 841 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 842 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 843 | "shasum": "" 844 | }, 845 | "require": { 846 | "php": "^5.6 || ^7.0" 847 | }, 848 | "require-dev": { 849 | "phpunit/phpunit": "^5.7 || ^6.0" 850 | }, 851 | "type": "library", 852 | "extra": { 853 | "branch-alias": { 854 | "dev-master": "1.0.x-dev" 855 | } 856 | }, 857 | "autoload": { 858 | "classmap": [ 859 | "src/" 860 | ] 861 | }, 862 | "notification-url": "https://packagist.org/downloads/", 863 | "license": [ 864 | "BSD-3-Clause" 865 | ], 866 | "authors": [ 867 | { 868 | "name": "Sebastian Bergmann", 869 | "email": "sebastian@phpunit.de" 870 | } 871 | ], 872 | "description": "Looks up which function or method a line of code belongs to", 873 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 874 | "time": "2017-03-04T06:30:41+00:00" 875 | }, 876 | { 877 | "name": "sebastian/comparator", 878 | "version": "3.0.2", 879 | "source": { 880 | "type": "git", 881 | "url": "https://github.com/sebastianbergmann/comparator.git", 882 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 883 | }, 884 | "dist": { 885 | "type": "zip", 886 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 887 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 888 | "shasum": "" 889 | }, 890 | "require": { 891 | "php": "^7.1", 892 | "sebastian/diff": "^3.0", 893 | "sebastian/exporter": "^3.1" 894 | }, 895 | "require-dev": { 896 | "phpunit/phpunit": "^7.1" 897 | }, 898 | "type": "library", 899 | "extra": { 900 | "branch-alias": { 901 | "dev-master": "3.0-dev" 902 | } 903 | }, 904 | "autoload": { 905 | "classmap": [ 906 | "src/" 907 | ] 908 | }, 909 | "notification-url": "https://packagist.org/downloads/", 910 | "license": [ 911 | "BSD-3-Clause" 912 | ], 913 | "authors": [ 914 | { 915 | "name": "Jeff Welch", 916 | "email": "whatthejeff@gmail.com" 917 | }, 918 | { 919 | "name": "Volker Dusch", 920 | "email": "github@wallbash.com" 921 | }, 922 | { 923 | "name": "Bernhard Schussek", 924 | "email": "bschussek@2bepublished.at" 925 | }, 926 | { 927 | "name": "Sebastian Bergmann", 928 | "email": "sebastian@phpunit.de" 929 | } 930 | ], 931 | "description": "Provides the functionality to compare PHP values for equality", 932 | "homepage": "https://github.com/sebastianbergmann/comparator", 933 | "keywords": [ 934 | "comparator", 935 | "compare", 936 | "equality" 937 | ], 938 | "time": "2018-07-12T15:12:46+00:00" 939 | }, 940 | { 941 | "name": "sebastian/diff", 942 | "version": "3.0.2", 943 | "source": { 944 | "type": "git", 945 | "url": "https://github.com/sebastianbergmann/diff.git", 946 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 947 | }, 948 | "dist": { 949 | "type": "zip", 950 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 951 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 952 | "shasum": "" 953 | }, 954 | "require": { 955 | "php": "^7.1" 956 | }, 957 | "require-dev": { 958 | "phpunit/phpunit": "^7.5 || ^8.0", 959 | "symfony/process": "^2 || ^3.3 || ^4" 960 | }, 961 | "type": "library", 962 | "extra": { 963 | "branch-alias": { 964 | "dev-master": "3.0-dev" 965 | } 966 | }, 967 | "autoload": { 968 | "classmap": [ 969 | "src/" 970 | ] 971 | }, 972 | "notification-url": "https://packagist.org/downloads/", 973 | "license": [ 974 | "BSD-3-Clause" 975 | ], 976 | "authors": [ 977 | { 978 | "name": "Kore Nordmann", 979 | "email": "mail@kore-nordmann.de" 980 | }, 981 | { 982 | "name": "Sebastian Bergmann", 983 | "email": "sebastian@phpunit.de" 984 | } 985 | ], 986 | "description": "Diff implementation", 987 | "homepage": "https://github.com/sebastianbergmann/diff", 988 | "keywords": [ 989 | "diff", 990 | "udiff", 991 | "unidiff", 992 | "unified diff" 993 | ], 994 | "time": "2019-02-04T06:01:07+00:00" 995 | }, 996 | { 997 | "name": "sebastian/environment", 998 | "version": "4.2.3", 999 | "source": { 1000 | "type": "git", 1001 | "url": "https://github.com/sebastianbergmann/environment.git", 1002 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" 1003 | }, 1004 | "dist": { 1005 | "type": "zip", 1006 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1007 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1008 | "shasum": "" 1009 | }, 1010 | "require": { 1011 | "php": "^7.1" 1012 | }, 1013 | "require-dev": { 1014 | "phpunit/phpunit": "^7.5" 1015 | }, 1016 | "suggest": { 1017 | "ext-posix": "*" 1018 | }, 1019 | "type": "library", 1020 | "extra": { 1021 | "branch-alias": { 1022 | "dev-master": "4.2-dev" 1023 | } 1024 | }, 1025 | "autoload": { 1026 | "classmap": [ 1027 | "src/" 1028 | ] 1029 | }, 1030 | "notification-url": "https://packagist.org/downloads/", 1031 | "license": [ 1032 | "BSD-3-Clause" 1033 | ], 1034 | "authors": [ 1035 | { 1036 | "name": "Sebastian Bergmann", 1037 | "email": "sebastian@phpunit.de" 1038 | } 1039 | ], 1040 | "description": "Provides functionality to handle HHVM/PHP environments", 1041 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1042 | "keywords": [ 1043 | "Xdebug", 1044 | "environment", 1045 | "hhvm" 1046 | ], 1047 | "time": "2019-11-20T08:46:58+00:00" 1048 | }, 1049 | { 1050 | "name": "sebastian/exporter", 1051 | "version": "3.1.2", 1052 | "source": { 1053 | "type": "git", 1054 | "url": "https://github.com/sebastianbergmann/exporter.git", 1055 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" 1056 | }, 1057 | "dist": { 1058 | "type": "zip", 1059 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", 1060 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", 1061 | "shasum": "" 1062 | }, 1063 | "require": { 1064 | "php": "^7.0", 1065 | "sebastian/recursion-context": "^3.0" 1066 | }, 1067 | "require-dev": { 1068 | "ext-mbstring": "*", 1069 | "phpunit/phpunit": "^6.0" 1070 | }, 1071 | "type": "library", 1072 | "extra": { 1073 | "branch-alias": { 1074 | "dev-master": "3.1.x-dev" 1075 | } 1076 | }, 1077 | "autoload": { 1078 | "classmap": [ 1079 | "src/" 1080 | ] 1081 | }, 1082 | "notification-url": "https://packagist.org/downloads/", 1083 | "license": [ 1084 | "BSD-3-Clause" 1085 | ], 1086 | "authors": [ 1087 | { 1088 | "name": "Sebastian Bergmann", 1089 | "email": "sebastian@phpunit.de" 1090 | }, 1091 | { 1092 | "name": "Jeff Welch", 1093 | "email": "whatthejeff@gmail.com" 1094 | }, 1095 | { 1096 | "name": "Volker Dusch", 1097 | "email": "github@wallbash.com" 1098 | }, 1099 | { 1100 | "name": "Adam Harvey", 1101 | "email": "aharvey@php.net" 1102 | }, 1103 | { 1104 | "name": "Bernhard Schussek", 1105 | "email": "bschussek@gmail.com" 1106 | } 1107 | ], 1108 | "description": "Provides the functionality to export PHP variables for visualization", 1109 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1110 | "keywords": [ 1111 | "export", 1112 | "exporter" 1113 | ], 1114 | "time": "2019-09-14T09:02:43+00:00" 1115 | }, 1116 | { 1117 | "name": "sebastian/global-state", 1118 | "version": "3.0.0", 1119 | "source": { 1120 | "type": "git", 1121 | "url": "https://github.com/sebastianbergmann/global-state.git", 1122 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" 1123 | }, 1124 | "dist": { 1125 | "type": "zip", 1126 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1127 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1128 | "shasum": "" 1129 | }, 1130 | "require": { 1131 | "php": "^7.2", 1132 | "sebastian/object-reflector": "^1.1.1", 1133 | "sebastian/recursion-context": "^3.0" 1134 | }, 1135 | "require-dev": { 1136 | "ext-dom": "*", 1137 | "phpunit/phpunit": "^8.0" 1138 | }, 1139 | "suggest": { 1140 | "ext-uopz": "*" 1141 | }, 1142 | "type": "library", 1143 | "extra": { 1144 | "branch-alias": { 1145 | "dev-master": "3.0-dev" 1146 | } 1147 | }, 1148 | "autoload": { 1149 | "classmap": [ 1150 | "src/" 1151 | ] 1152 | }, 1153 | "notification-url": "https://packagist.org/downloads/", 1154 | "license": [ 1155 | "BSD-3-Clause" 1156 | ], 1157 | "authors": [ 1158 | { 1159 | "name": "Sebastian Bergmann", 1160 | "email": "sebastian@phpunit.de" 1161 | } 1162 | ], 1163 | "description": "Snapshotting of global state", 1164 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1165 | "keywords": [ 1166 | "global state" 1167 | ], 1168 | "time": "2019-02-01T05:30:01+00:00" 1169 | }, 1170 | { 1171 | "name": "sebastian/object-enumerator", 1172 | "version": "3.0.3", 1173 | "source": { 1174 | "type": "git", 1175 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1176 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1177 | }, 1178 | "dist": { 1179 | "type": "zip", 1180 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1181 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1182 | "shasum": "" 1183 | }, 1184 | "require": { 1185 | "php": "^7.0", 1186 | "sebastian/object-reflector": "^1.1.1", 1187 | "sebastian/recursion-context": "^3.0" 1188 | }, 1189 | "require-dev": { 1190 | "phpunit/phpunit": "^6.0" 1191 | }, 1192 | "type": "library", 1193 | "extra": { 1194 | "branch-alias": { 1195 | "dev-master": "3.0.x-dev" 1196 | } 1197 | }, 1198 | "autoload": { 1199 | "classmap": [ 1200 | "src/" 1201 | ] 1202 | }, 1203 | "notification-url": "https://packagist.org/downloads/", 1204 | "license": [ 1205 | "BSD-3-Clause" 1206 | ], 1207 | "authors": [ 1208 | { 1209 | "name": "Sebastian Bergmann", 1210 | "email": "sebastian@phpunit.de" 1211 | } 1212 | ], 1213 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1214 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1215 | "time": "2017-08-03T12:35:26+00:00" 1216 | }, 1217 | { 1218 | "name": "sebastian/object-reflector", 1219 | "version": "1.1.1", 1220 | "source": { 1221 | "type": "git", 1222 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1223 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1224 | }, 1225 | "dist": { 1226 | "type": "zip", 1227 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1228 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1229 | "shasum": "" 1230 | }, 1231 | "require": { 1232 | "php": "^7.0" 1233 | }, 1234 | "require-dev": { 1235 | "phpunit/phpunit": "^6.0" 1236 | }, 1237 | "type": "library", 1238 | "extra": { 1239 | "branch-alias": { 1240 | "dev-master": "1.1-dev" 1241 | } 1242 | }, 1243 | "autoload": { 1244 | "classmap": [ 1245 | "src/" 1246 | ] 1247 | }, 1248 | "notification-url": "https://packagist.org/downloads/", 1249 | "license": [ 1250 | "BSD-3-Clause" 1251 | ], 1252 | "authors": [ 1253 | { 1254 | "name": "Sebastian Bergmann", 1255 | "email": "sebastian@phpunit.de" 1256 | } 1257 | ], 1258 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1259 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1260 | "time": "2017-03-29T09:07:27+00:00" 1261 | }, 1262 | { 1263 | "name": "sebastian/recursion-context", 1264 | "version": "3.0.0", 1265 | "source": { 1266 | "type": "git", 1267 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1268 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1269 | }, 1270 | "dist": { 1271 | "type": "zip", 1272 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1273 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1274 | "shasum": "" 1275 | }, 1276 | "require": { 1277 | "php": "^7.0" 1278 | }, 1279 | "require-dev": { 1280 | "phpunit/phpunit": "^6.0" 1281 | }, 1282 | "type": "library", 1283 | "extra": { 1284 | "branch-alias": { 1285 | "dev-master": "3.0.x-dev" 1286 | } 1287 | }, 1288 | "autoload": { 1289 | "classmap": [ 1290 | "src/" 1291 | ] 1292 | }, 1293 | "notification-url": "https://packagist.org/downloads/", 1294 | "license": [ 1295 | "BSD-3-Clause" 1296 | ], 1297 | "authors": [ 1298 | { 1299 | "name": "Jeff Welch", 1300 | "email": "whatthejeff@gmail.com" 1301 | }, 1302 | { 1303 | "name": "Sebastian Bergmann", 1304 | "email": "sebastian@phpunit.de" 1305 | }, 1306 | { 1307 | "name": "Adam Harvey", 1308 | "email": "aharvey@php.net" 1309 | } 1310 | ], 1311 | "description": "Provides functionality to recursively process PHP variables", 1312 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1313 | "time": "2017-03-03T06:23:57+00:00" 1314 | }, 1315 | { 1316 | "name": "sebastian/resource-operations", 1317 | "version": "2.0.1", 1318 | "source": { 1319 | "type": "git", 1320 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1321 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 1322 | }, 1323 | "dist": { 1324 | "type": "zip", 1325 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1326 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1327 | "shasum": "" 1328 | }, 1329 | "require": { 1330 | "php": "^7.1" 1331 | }, 1332 | "type": "library", 1333 | "extra": { 1334 | "branch-alias": { 1335 | "dev-master": "2.0-dev" 1336 | } 1337 | }, 1338 | "autoload": { 1339 | "classmap": [ 1340 | "src/" 1341 | ] 1342 | }, 1343 | "notification-url": "https://packagist.org/downloads/", 1344 | "license": [ 1345 | "BSD-3-Clause" 1346 | ], 1347 | "authors": [ 1348 | { 1349 | "name": "Sebastian Bergmann", 1350 | "email": "sebastian@phpunit.de" 1351 | } 1352 | ], 1353 | "description": "Provides a list of PHP built-in functions that operate on resources", 1354 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1355 | "time": "2018-10-04T04:07:39+00:00" 1356 | }, 1357 | { 1358 | "name": "sebastian/type", 1359 | "version": "1.1.3", 1360 | "source": { 1361 | "type": "git", 1362 | "url": "https://github.com/sebastianbergmann/type.git", 1363 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" 1364 | }, 1365 | "dist": { 1366 | "type": "zip", 1367 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", 1368 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", 1369 | "shasum": "" 1370 | }, 1371 | "require": { 1372 | "php": "^7.2" 1373 | }, 1374 | "require-dev": { 1375 | "phpunit/phpunit": "^8.2" 1376 | }, 1377 | "type": "library", 1378 | "extra": { 1379 | "branch-alias": { 1380 | "dev-master": "1.1-dev" 1381 | } 1382 | }, 1383 | "autoload": { 1384 | "classmap": [ 1385 | "src/" 1386 | ] 1387 | }, 1388 | "notification-url": "https://packagist.org/downloads/", 1389 | "license": [ 1390 | "BSD-3-Clause" 1391 | ], 1392 | "authors": [ 1393 | { 1394 | "name": "Sebastian Bergmann", 1395 | "email": "sebastian@phpunit.de", 1396 | "role": "lead" 1397 | } 1398 | ], 1399 | "description": "Collection of value objects that represent the types of the PHP type system", 1400 | "homepage": "https://github.com/sebastianbergmann/type", 1401 | "time": "2019-07-02T08:10:15+00:00" 1402 | }, 1403 | { 1404 | "name": "sebastian/version", 1405 | "version": "2.0.1", 1406 | "source": { 1407 | "type": "git", 1408 | "url": "https://github.com/sebastianbergmann/version.git", 1409 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1410 | }, 1411 | "dist": { 1412 | "type": "zip", 1413 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1414 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1415 | "shasum": "" 1416 | }, 1417 | "require": { 1418 | "php": ">=5.6" 1419 | }, 1420 | "type": "library", 1421 | "extra": { 1422 | "branch-alias": { 1423 | "dev-master": "2.0.x-dev" 1424 | } 1425 | }, 1426 | "autoload": { 1427 | "classmap": [ 1428 | "src/" 1429 | ] 1430 | }, 1431 | "notification-url": "https://packagist.org/downloads/", 1432 | "license": [ 1433 | "BSD-3-Clause" 1434 | ], 1435 | "authors": [ 1436 | { 1437 | "name": "Sebastian Bergmann", 1438 | "email": "sebastian@phpunit.de", 1439 | "role": "lead" 1440 | } 1441 | ], 1442 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1443 | "homepage": "https://github.com/sebastianbergmann/version", 1444 | "time": "2016-10-03T07:35:21+00:00" 1445 | }, 1446 | { 1447 | "name": "symfony/polyfill-ctype", 1448 | "version": "v1.18.1", 1449 | "source": { 1450 | "type": "git", 1451 | "url": "https://github.com/symfony/polyfill-ctype.git", 1452 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" 1453 | }, 1454 | "dist": { 1455 | "type": "zip", 1456 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", 1457 | "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", 1458 | "shasum": "" 1459 | }, 1460 | "require": { 1461 | "php": ">=5.3.3" 1462 | }, 1463 | "suggest": { 1464 | "ext-ctype": "For best performance" 1465 | }, 1466 | "type": "library", 1467 | "extra": { 1468 | "branch-alias": { 1469 | "dev-master": "1.18-dev" 1470 | }, 1471 | "thanks": { 1472 | "name": "symfony/polyfill", 1473 | "url": "https://github.com/symfony/polyfill" 1474 | } 1475 | }, 1476 | "autoload": { 1477 | "psr-4": { 1478 | "Symfony\\Polyfill\\Ctype\\": "" 1479 | }, 1480 | "files": [ 1481 | "bootstrap.php" 1482 | ] 1483 | }, 1484 | "notification-url": "https://packagist.org/downloads/", 1485 | "license": [ 1486 | "MIT" 1487 | ], 1488 | "authors": [ 1489 | { 1490 | "name": "Gert de Pagter", 1491 | "email": "BackEndTea@gmail.com" 1492 | }, 1493 | { 1494 | "name": "Symfony Community", 1495 | "homepage": "https://symfony.com/contributors" 1496 | } 1497 | ], 1498 | "description": "Symfony polyfill for ctype functions", 1499 | "homepage": "https://symfony.com", 1500 | "keywords": [ 1501 | "compatibility", 1502 | "ctype", 1503 | "polyfill", 1504 | "portable" 1505 | ], 1506 | "funding": [ 1507 | { 1508 | "url": "https://symfony.com/sponsor", 1509 | "type": "custom" 1510 | }, 1511 | { 1512 | "url": "https://github.com/fabpot", 1513 | "type": "github" 1514 | }, 1515 | { 1516 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1517 | "type": "tidelift" 1518 | } 1519 | ], 1520 | "time": "2020-07-14T12:35:20+00:00" 1521 | }, 1522 | { 1523 | "name": "theseer/tokenizer", 1524 | "version": "1.2.0", 1525 | "source": { 1526 | "type": "git", 1527 | "url": "https://github.com/theseer/tokenizer.git", 1528 | "reference": "75a63c33a8577608444246075ea0af0d052e452a" 1529 | }, 1530 | "dist": { 1531 | "type": "zip", 1532 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", 1533 | "reference": "75a63c33a8577608444246075ea0af0d052e452a", 1534 | "shasum": "" 1535 | }, 1536 | "require": { 1537 | "ext-dom": "*", 1538 | "ext-tokenizer": "*", 1539 | "ext-xmlwriter": "*", 1540 | "php": "^7.2 || ^8.0" 1541 | }, 1542 | "type": "library", 1543 | "autoload": { 1544 | "classmap": [ 1545 | "src/" 1546 | ] 1547 | }, 1548 | "notification-url": "https://packagist.org/downloads/", 1549 | "license": [ 1550 | "BSD-3-Clause" 1551 | ], 1552 | "authors": [ 1553 | { 1554 | "name": "Arne Blankerts", 1555 | "email": "arne@blankerts.de", 1556 | "role": "Developer" 1557 | } 1558 | ], 1559 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1560 | "funding": [ 1561 | { 1562 | "url": "https://github.com/theseer", 1563 | "type": "github" 1564 | } 1565 | ], 1566 | "time": "2020-07-12T23:59:07+00:00" 1567 | }, 1568 | { 1569 | "name": "webmozart/assert", 1570 | "version": "1.9.1", 1571 | "source": { 1572 | "type": "git", 1573 | "url": "https://github.com/webmozart/assert.git", 1574 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" 1575 | }, 1576 | "dist": { 1577 | "type": "zip", 1578 | "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", 1579 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", 1580 | "shasum": "" 1581 | }, 1582 | "require": { 1583 | "php": "^5.3.3 || ^7.0 || ^8.0", 1584 | "symfony/polyfill-ctype": "^1.8" 1585 | }, 1586 | "conflict": { 1587 | "phpstan/phpstan": "<0.12.20", 1588 | "vimeo/psalm": "<3.9.1" 1589 | }, 1590 | "require-dev": { 1591 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1592 | }, 1593 | "type": "library", 1594 | "autoload": { 1595 | "psr-4": { 1596 | "Webmozart\\Assert\\": "src/" 1597 | } 1598 | }, 1599 | "notification-url": "https://packagist.org/downloads/", 1600 | "license": [ 1601 | "MIT" 1602 | ], 1603 | "authors": [ 1604 | { 1605 | "name": "Bernhard Schussek", 1606 | "email": "bschussek@gmail.com" 1607 | } 1608 | ], 1609 | "description": "Assertions to validate method input/output with nice error messages.", 1610 | "keywords": [ 1611 | "assert", 1612 | "check", 1613 | "validate" 1614 | ], 1615 | "time": "2020-07-08T17:02:28+00:00" 1616 | } 1617 | ], 1618 | "aliases": [], 1619 | "minimum-stability": "stable", 1620 | "stability-flags": [], 1621 | "prefer-stable": false, 1622 | "prefer-lowest": false, 1623 | "platform": [], 1624 | "platform-dev": [], 1625 | "plugin-api-version": "1.1.0" 1626 | } 1627 | --------------------------------------------------------------------------------