├── LICENSE ├── README.md ├── composer.json └── src ├── Driver.php ├── Driver ├── Igbinary.php ├── Json.php ├── MsgPack.php └── Txt.php ├── DriverInterface.php ├── Exception ├── Driver │ ├── DriverException.php │ ├── ExtensionNotLoadedException.php │ ├── FileNotFoundException.php │ ├── FileNotReadableException.php │ └── FileNotWritableException.php ├── Exception.php ├── Query │ ├── BadFunctionException.php │ ├── ColumnsNotFoundException.php │ ├── ColumnsValueException.php │ ├── OperatorNotFound.php │ ├── QueryException.php │ └── TableNotFoundException.php └── TableBuilder │ ├── ColumnsNotFoundException.php │ ├── ColumnsValueException.php │ └── TableBuilderException.php ├── Field.php ├── Field ├── BoolType.php ├── CharType.php ├── DateTimeType.php ├── DateType.php ├── DropType.php ├── FloatType.php ├── IncrementType.php ├── IntType.php ├── RenameType.php ├── StringType.php └── TextType.php ├── Request.php ├── RequestHandler.php ├── RequestInterface.php ├── Schema.php ├── Table.php ├── TableAlter.php ├── TableBuilder.php ├── ValueToString.php ├── Where.php └── WhereHandler.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Noel Mathieu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Queryflatfile 2 | 3 | [![Build Status](https://github.com/soosyze/queryflatfile/workflows/Tests/badge.svg?branch=master)](https://github.com/soosyze/queryflatfile/actions?query=branch:master "Tests") 4 | [![Coverage Status](https://coveralls.io/repos/github/soosyze/queryflatfile/badge.svg?branch=master)](https://coveralls.io/github/soosyze/queryflatfile?branch=master "Coveralls") 5 | [![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/soosyze/queryflatfile/blob/master/LICENSE "LICENSE") 6 | [![Packagist](https://img.shields.io/packagist/v/soosyze/queryflatfile.svg)](https://packagist.org/packages/soosyze/queryflatfile "Packagist") 7 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/soosyze/queryflatfile.svg)](/README.md#version-php "PHP version 7.2 minimum") 8 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/soosyze/queryflatfile.svg)](https://github.com/soosyze/queryflatfile/archive/master.zip "Download") 9 | 10 | - :gb: [README in English](README.md) 11 | - :fr: [README en Français](README_fr.md) 12 | 13 | ## About 14 | 15 | Queryflatfile is a flat file database library written in PHP. 16 | Stores your data by default in `JSON` format, also supports `txt`, [msgPack](https://pecl.php.net/package/msgpack) and [igbinary](https://pecl.php.net/package/igbinary) formats. 17 | Manipulate your data with a QueryBuilder similar to SQL syntax. 18 | 19 | ## Summary 20 | 21 | - [Requirements](/README.md#requirements) 22 | - [Installation](/README.md#installation) 23 | - [Simple example](/README.md#simple-exemple) 24 | - [Methods](/README.md#methods) 25 | - [Usage](/README.md#usage) 26 | - [License](/README.md#license) 27 | 28 | ## Requirements 29 | 30 | ### PHP version 31 | 32 | | Version PHP | QueryFlatFile 3.1.x | 33 | | --------------- | ------------------- | 34 | | <= 7.1 | ✗ Unsupported | 35 | | 7.2 / 7.3 / 7.4 | ✓ Supported | 36 | | 8.0 / 8.1 / 8.2 | ✓ Supported | 37 | 38 | ### Extensions 39 | 40 | - `txt` for recording data with PHP serialize, 41 | - `json` for recording data in JSON format, 42 | - [msgPack](https://pecl.php.net/package/msgpack) for recording data in binary. 43 | - [igbinary](https://pecl.php.net/package/igbinary) for recording data in binary. 44 | 45 | ### Memory required 46 | 47 | The minimum amount of memory required depends on the amount of data you are going to process and the type of operations. 48 | 49 | ### Permission of files and directory 50 | 51 | Permission to write and read files in the directory that will store your data. 52 | 53 | ## Installation 54 | 55 | ### Composer 56 | 57 | To install **Queryflatfile** via Composer you must have the installer or the binary file [Composer](https://getcomposer.org/download/) 58 | 59 | Go to your project directory, open a command prompt and run the following command: 60 | 61 | ```sh 62 | composer require soosyze/queryflatfile --no-dev 63 | ``` 64 | 65 | Or, if you use the binary file, 66 | 67 | ```sh 68 | php composer.phar require soosyze/queryflatfile --no-dev 69 | ``` 70 | 71 | ## Simple example 72 | 73 | ```php 74 | require __DIR__ . '/vendor/autoload.php'; 75 | 76 | use Soosyze\Queryflatfile\Schema; 77 | use Soosyze\Queryflatfile\Request; 78 | use Soosyze\Queryflatfile\TableBuilder; 79 | use Soosyze\Queryflatfile\Driver\Json; 80 | 81 | $sch = new Schema(__DIR__ . 'data', 'schema', new Json()); 82 | $req = new Request($sch); 83 | 84 | $sch->createTableIfNotExists('user', function(TableBuilder $table): void { 85 | $table->increments('id') 86 | $table->string('name') 87 | $table->string('firstname')->nullable(); 88 | }); 89 | 90 | $req->insertInto('user', [ 'name', 'firstname' ]) 91 | ->values([ 'NOEL', 'Mathieu' ]) 92 | ->values([ 'DUPOND', 'Jean' ]) 93 | ->values([ 'MARTIN', null ]) 94 | ->execute(); 95 | 96 | $data = $req->select('id', 'name') 97 | ->from('user') 98 | ->where('firstname', '=', 'Jean') 99 | ->fetch(); 100 | 101 | print_r($data); 102 | 103 | $sch->dropTableIfExists('user'); 104 | ``` 105 | 106 | The above example will output: 107 | 108 | ``` 109 | Array 110 | ( 111 | [id] => 2 112 | [name] => DUPOND 113 | ) 114 | ``` 115 | 116 | ## Methods 117 | 118 | **Schema** 119 | 120 | - `dropSchema()`, 121 | - `getIncrement( string $tableName )`, 122 | - `getSchema()`, 123 | - `getTableSchema( string $tableName )`, 124 | - `hasColumn( string $tableName, $columnName )`, 125 | - `hasTable( string $tableName )`, 126 | - `setConfig( string $host, string $name = 'schema', DriverInterface $driver = null )`. 127 | 128 | **Handling tables** 129 | 130 | - `alterTable( string $tableName, callable $callback )`, 131 | - `createTable( string $tableName, callable $callback = null )`, 132 | - `createTableIfNotExists( string $tableName, callable $callback = null )` : 133 | - `boolean( string $name )`, 134 | - `char( string $name, $length = 1)`, 135 | - `date( string $name )`, 136 | - `dateTime( string $name )`, 137 | - `float( string $name )`, 138 | - `increments( string $name )`, 139 | - `integer( string $name )`, 140 | - `string( string $name, $length = 255)`, 141 | - `text( string $name )`. 142 | - `dropTable( string $tableName )`, 143 | - `dropTableIfExists( string $tableName )`, 144 | - `truncateTable( string $tableName )`. 145 | 146 | **Selection request** 147 | 148 | - `select( string ...$columnNames )`, 149 | - `from( string $tableName )`, 150 | - `leftJoin( string $tableName, \Closure|string $column, string $condition = '', string $value = '' )`, 151 | - `rightJoin( string $tableName, \Closure|string $column, string $condition = '', string $value = '' )`, 152 | - `union( RequestInterface $union )`, 153 | - `unionAll( RequestInterface $union )`, 154 | - `orderBy( string $columnName, int $order = SORT_DESC|SORT_ASC )`, 155 | - `limit( int $limit, int $offset = 0 )`. 156 | 157 | **Request for execution** 158 | 159 | - `insertInto( string $tableName, array $columnNames )`, 160 | - `values( array $rowValues )`, 161 | - `update( string $tableName, array $row )`, 162 | - `delete()`, 163 | - `execute()` Performs the insertion, modification and deletion of data. 164 | 165 | **Result(s) of the query** 166 | 167 | - `fetch(): array` Returns the first result of the query, 168 | - `fetchAll(): array` Returns all the results of the query, 169 | - `lists( string $columnName, string $key = null ): array` Returns a list of the column passed in parameter. 170 | 171 | **Where** 172 | 173 | - `where( string $columnName, string $condition, null|scalar $value )`, 174 | - `orWhere( string $columnName, string $condition, null|scalar $value )`, 175 | - `notWhere( string $columnName, string $condition, null|scalar $value )`, 176 | - `orNotWhere( string $columnName, string $condition, null|scalar $value )`. 177 | 178 | Supported conditions (===, ==, !=, <>, <, <=, >, >=, like, ilike, not like, not ilike) 179 | 180 | **Where** 181 | 182 | - `whereGroup( \Closure $columnName )`, 183 | - `orWhereGroup( \Closure $columnName )`, 184 | - `notWhereGroup( \Closure $columnName )`, 185 | - `orNotWhereGroup( \Closure $columnName )`. 186 | 187 | **Where between** 188 | 189 | - `between( string $columnName, $min, $max )`, 190 | - `orBetween( string $columnName, $min, $max )`, 191 | - `notBetween( string $columnName, $min, $max )`, 192 | - `orNotBetween( string $columnName, $min, $max )`. 193 | 194 | **Where in** 195 | 196 | - `in( string $columnName, array $values )`, 197 | - `orIn( string $columnName, array $values )`, 198 | - `notIn( string $columnName, array $values )`, 199 | - `orNotIn( string $columnName, array $values )`. 200 | 201 | **Where isNull** 202 | 203 | - `isNull( string $columnName )`, 204 | - `orIsNull( string $columnName )`, 205 | - `isNotNull( string $columnName )`, 206 | - `orIsNotNull( string $columnName )`. 207 | 208 | **Where regex** 209 | 210 | - `regex( string $columnName, string $pattern )`, 211 | - `orRegex( string $columnName, string $pattern )`, 212 | - `notRegex( string $columnName, string $pattern )`, 213 | - `orNotRegex( string $columnName, string $pattern )`. 214 | 215 | ## Usage 216 | 217 | For examples of uses, refer to the [user documentation](/USAGE.md). 218 | 219 | ## License 220 | 221 | This project is licensed under the [MIT license](/LICENSE). 222 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soosyze/queryflatfile", 3 | "description": "The Queryflatfile is PHP library for simple database not SQL", 4 | "type": "library", 5 | "keywords": ["php", "library", "php-library", "nosql", "db", "database", "flat file", "db flat file", "query-builder", "composer"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Mathieu NOËL", 10 | "email": "mathieu@soosyze.com", 11 | "homepage": "http://mathieu-noel.fr" 12 | } 13 | ], 14 | "support": { 15 | "email": "mathieu@soosyze.com" 16 | }, 17 | "require": { 18 | "php": ">=7.2" 19 | }, 20 | "require-dev": { 21 | "soosyze/php-cs-fixer-config": "^1.0", 22 | "phpstan/phpstan": "^1.1", 23 | "phpstan/phpstan-phpunit": "^1.0", 24 | "phpunit/phpunit": "^8.5", 25 | "rector/rector": "^0.12" 26 | }, 27 | "suggest": { 28 | "phpdocumentor/phpdocumentor": "To generate documentation.", 29 | "phpmetrics/phpmetrics": "To generate a code status report in HTML format." 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Soosyze\\Queryflatfile\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Soosyze\\Queryflatfile\\Tests\\": "tests" 39 | } 40 | }, 41 | "config": { 42 | "bin-dir": "bin", 43 | "sort-packages": true, 44 | "optimize-autoloader": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | abstract class Driver implements DriverInterface 24 | { 25 | const DS = DIRECTORY_SEPARATOR; 26 | 27 | /** 28 | * Déclenche une exception si l'extension du fichier n'est pas chargée. 29 | * 30 | * @codeCoverageIgnore has 31 | * 32 | * @throws Exception\Driver\ExtensionNotLoadedException 33 | * 34 | * @return void 35 | */ 36 | abstract public function checkExtension(): void; 37 | 38 | /** 39 | * Renvoie les données séréalisées. 40 | * 41 | * @param array $data 42 | * 43 | * @return string 44 | */ 45 | abstract public function serializeData(array $data): string; 46 | 47 | /** 48 | * Renvoie les données désérialisées. 49 | * 50 | * @param string $data 51 | * 52 | * @return array 53 | */ 54 | abstract public function unserializeData(string $data): array; 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function create(string $path, string $fileName, array $data = []): bool 60 | { 61 | $this->checkExtension(); 62 | $file = $this->getFile($path, $fileName); 63 | 64 | if (!file_exists($path)) { 65 | mkdir($path, 0775, true); 66 | } 67 | if (file_exists($file)) { 68 | return false; 69 | } 70 | 71 | $handle = fopen($file, 'w+'); 72 | if ($handle === false) { 73 | throw new DriverException(sprintf('The %s file cannot be opened', $file)); 74 | } 75 | fwrite($handle, $this->serializeData($data)); 76 | 77 | return fclose($handle); 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public function read(string $path, string $fileName): array 84 | { 85 | $this->checkExtension(); 86 | $file = $this->getFile($path, $fileName); 87 | 88 | $this->isExist($file); 89 | $this->isRead($file); 90 | 91 | $data = file_get_contents($file); 92 | 93 | return $data === false 94 | ? [] 95 | : $this->unserializeData($data); 96 | } 97 | 98 | /** 99 | * {@inheritDoc} 100 | */ 101 | public function save(string $path, string $fileName, array $data): bool 102 | { 103 | $this->checkExtension(); 104 | $file = $this->getFile($path, $fileName); 105 | 106 | $this->isExist($file); 107 | $this->isWrite($file); 108 | 109 | $handle = fopen($file, 'w'); 110 | if ($handle === false) { 111 | throw new DriverException(sprintf('The %s file cannot be opened', $file)); 112 | } 113 | fwrite($handle, $this->serializeData($data)); 114 | 115 | return fclose($handle); 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | */ 121 | public function delete(string $path, string $fileName): bool 122 | { 123 | return unlink($this->getFile($path, $fileName)); 124 | } 125 | 126 | /** 127 | * {@inheritDoc} 128 | */ 129 | public function has(string $path, string $fileName): bool 130 | { 131 | return file_exists($this->getFile($path, $fileName)); 132 | } 133 | 134 | /** 135 | * Concatène le chemin, le nom du fichier et l'extension. 136 | * 137 | * @param string $path Chemin de la table. 138 | * @param string $fileName Nom du fichier. 139 | * 140 | * @return string Chemin complet du fichier. 141 | */ 142 | public function getFile(string $path, string $fileName): string 143 | { 144 | $file = $path . self::DS . $fileName . '.' . $this->getExtension(); 145 | 146 | return str_replace('\\', self::DS, $file); 147 | } 148 | 149 | /** 150 | * Déclenche une exception si le fichier passé en paramètre d'existe pas. 151 | * 152 | * @param string $file Chemin complet du fichier. 153 | * 154 | * @throws FileNotFoundException 155 | * 156 | * @return void 157 | */ 158 | protected function isExist(string $file): void 159 | { 160 | if (!file_exists($file)) { 161 | throw new FileNotFoundException(sprintf('The %s file is missing.', $file)); 162 | } 163 | } 164 | 165 | /** 166 | * Déclenche une exception si le fichier passé en paramètre n'a pas le droit d'écriture. 167 | * 168 | * @codeCoverageIgnore has 169 | * 170 | * @param string $file Chemin complet du fichier. 171 | * 172 | * @throws FileNotWritableException 173 | * 174 | * @return void 175 | */ 176 | protected function isWrite(string $file): void 177 | { 178 | if (!\is_writable($file)) { 179 | throw new FileNotWritableException(sprintf('The %s file is not writable.', $file)); 180 | } 181 | } 182 | 183 | /** 184 | * Déclenche une exception si le fichier passé en paramètre n'a pas le droit d'être lu. 185 | * 186 | * @codeCoverageIgnore has 187 | * 188 | * @param string $file Chemin complet du fichier. 189 | * 190 | * @throws FileNotReadableException 191 | * 192 | * @return void 193 | */ 194 | protected function isRead(string $file): void 195 | { 196 | if (!\is_readable($file)) { 197 | throw new FileNotReadableException(sprintf('The %s file is not readable.', $file)); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Driver/Igbinary.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Igbinary extends \Soosyze\Queryflatfile\Driver 21 | { 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public function checkExtension(): void 26 | { 27 | if (!extension_loaded('igbinary')) { 28 | throw new ExtensionNotLoadedException('The igbinary extension is not loaded.'); 29 | } 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function getExtension(): string 36 | { 37 | return 'ig'; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function serializeData(array $data): string 44 | { 45 | $serializeData = igbinary_serialize($data); 46 | if (!is_string($serializeData)) { 47 | throw new \Exception('An error occurred in serializing the data.'); 48 | } 49 | 50 | return $serializeData; 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function unserializeData(string $data): array 57 | { 58 | $dataUnserialize = igbinary_unserialize($data); 59 | if (!is_array($dataUnserialize)) { 60 | throw new \Exception('An error occurred in deserializing the data.'); 61 | } 62 | 63 | return $dataUnserialize; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Driver/Json.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Json extends \Soosyze\Queryflatfile\Driver 21 | { 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public function checkExtension(): void 26 | { 27 | if (!extension_loaded('json')) { 28 | throw new ExtensionNotLoadedException('The json extension is not loaded.'); 29 | } 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function getExtension(): string 36 | { 37 | return 'json'; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function serializeData(array $data): string 44 | { 45 | $serializeData = json_encode($data, JSON_UNESCAPED_UNICODE); 46 | if (!is_string($serializeData)) { 47 | throw new \Exception('An error occurred in serializing the data.'); 48 | } 49 | 50 | return $serializeData; 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function unserializeData(string $data): array 57 | { 58 | $dataUnserialize = json_decode($data, true, 512, JSON_UNESCAPED_UNICODE); 59 | if (!is_array($dataUnserialize)) { 60 | throw new \Exception('An error occurred in deserializing the data.'); 61 | } 62 | 63 | return $dataUnserialize; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Driver/MsgPack.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class MsgPack extends \Soosyze\Queryflatfile\Driver 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function checkExtension(): void 28 | { 29 | if (!extension_loaded('msgpack')) { 30 | throw new ExtensionNotLoadedException('The msgpack extension is not loaded.'); 31 | } 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function getExtension(): string 38 | { 39 | return 'msg'; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function serializeData(array $data): string 46 | { 47 | return msgpack_pack($data); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function unserializeData(string $data): array 54 | { 55 | $dataUnserialize = msgpack_unpack($data); 56 | if (!is_array($dataUnserialize)) { 57 | throw new \Exception('An error occurred in deserializing the data.'); 58 | } 59 | 60 | return $dataUnserialize; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Driver/Txt.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class Txt extends \Soosyze\Queryflatfile\Driver 19 | { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function checkExtension(): void 24 | { 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function getExtension(): string 31 | { 32 | return 'txt'; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function serializeData(array $data): string 39 | { 40 | return serialize($data); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function unserializeData(string $data): array 47 | { 48 | $dataUnserialize = unserialize($data); 49 | if (!is_array($dataUnserialize)) { 50 | throw new \Exception('An error occurred in deserializing the data.'); 51 | } 52 | 53 | return $dataUnserialize; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DriverInterface.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface DriverInterface 19 | { 20 | /** 21 | * Créer un fichier si celui-ci n'existe pas et enregistre des données 22 | * Les données DOIVENT conserver leur type. 23 | * 24 | * @param string $path Chemin du fichier. 25 | * @param string $fileName Nom du fichier SANS l'extension. 26 | * @param array $data Tableau associatif à enregistrer. 27 | * 28 | * @throws Exception\Driver\ExtensionNotLoadedException Si l'extension n'est pas chargée. 29 | * 30 | * @return bool TRUE si tous ce passe bien sinon FALSE. 31 | */ 32 | public function create(string $path, string $fileName, array $data = []): bool; 33 | 34 | /** 35 | * Lit un fichier et DOIT retourner son contenu sous forme de tableau associatif 36 | * quelle que soit sa profondeur. Les données DOIVENT conserver leur type. 37 | * 38 | * @param string $path Chemin du fichier. 39 | * @param string $fileName Nom du fichier SANS l'extension. 40 | * 41 | * @throws Exception\Driver\ExtensionNotLoadedException Si l'extension n'est pas chargée. 42 | * @throws Exception\Driver\FileNotFoundException Si le fichier est introuvable. 43 | * @throws Exception\Driver\FileNotReadableException Si le fichier n'a pas les droits suffisant pour être lu. 44 | * 45 | * @return array les données du fichier 46 | */ 47 | public function read(string $path, string $fileName): array; 48 | 49 | /** 50 | * Enregistre des données dans le fichier. 51 | * Les données DOIVENT conserver leur type. 52 | * 53 | * @param string $path Chemin du fichier. 54 | * @param string $fileName Nom du fichier SANS l'extension. 55 | * @param array $data Tableau associatif à enregistrer. 56 | * 57 | * @throws Exception\Driver\ExtensionNotLoadedException Si l'extension n'est pas chargée. 58 | * @throws Exception\Driver\FileNotFoundException Si le fichier est introuvable. 59 | * @throws Exception\Driver\FileNotWritableException Si le fichier n'a pas les droits suffisant pour être écrit. 60 | * 61 | * @return bool TRUE si tous ce passe bien sinon FALSE. 62 | */ 63 | public function save(string $path, string $fileName, array $data): bool; 64 | 65 | /** 66 | * Supprime un fichier. 67 | * 68 | * @param string $path Chemin du fichier. 69 | * @param string $fileName Nom du fichier SANS l'extension. 70 | * 71 | * @return bool TRUE si tous ce passe bien sinon FALSE. 72 | */ 73 | public function delete(string $path, string $fileName): bool; 74 | 75 | /** 76 | * Si le fichier existe. 77 | * 78 | * @param string $path Chemin du fichier. 79 | * @param string $fileName Nom du fichier SANS l'extension. 80 | * 81 | * @return bool 82 | */ 83 | public function has(string $path, string $fileName): bool; 84 | 85 | /** 86 | * Renseigne le nom de l'extension de fichier utilisé par le driver 87 | * au reste des composant. 88 | * 89 | * @return string Nom de l'extension SANS le point en préfix. 90 | */ 91 | public function getExtension(): string; 92 | } 93 | -------------------------------------------------------------------------------- /src/Exception/Driver/DriverException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DriverException extends \Soosyze\Queryflatfile\Exception\Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Driver/ExtensionNotLoadedException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ExtensionNotLoadedException extends DriverException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Driver/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FileNotFoundException extends DriverException 17 | { 18 | public function __construct( 19 | string $message = '', 20 | int $code = 0, 21 | \Throwable $previous = null 22 | ) { 23 | parent::__construct(str_replace('\\', '/', $message), $code, $previous); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception/Driver/FileNotReadableException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FileNotReadableException extends DriverException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Driver/FileNotWritableException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FileNotWritableException extends DriverException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Exception extends \Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/Query/BadFunctionException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class BadFunctionException extends QueryException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Query/ColumnsNotFoundException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ColumnsNotFoundException extends QueryException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Query/ColumnsValueException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ColumnsValueException extends QueryException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Query/OperatorNotFound.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class OperatorNotFound extends QueryException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Query/QueryException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class QueryException extends \Soosyze\Queryflatfile\Exception\Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/Query/TableNotFoundException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TableNotFoundException extends QueryException 17 | { 18 | public function __construct( 19 | string $tableName = '', 20 | int $code = 0, 21 | \Throwable $previous = null 22 | ) { 23 | parent::__construct( 24 | $tableName === '' 25 | ? 'Table is missing.' 26 | : sprintf('The %s table is missing.', $tableName), 27 | $code, 28 | $previous 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exception/TableBuilder/ColumnsNotFoundException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ColumnsNotFoundException extends TableBuilderException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/TableBuilder/ColumnsValueException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ColumnsValueException extends TableBuilderException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/TableBuilder/TableBuilderException.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TableBuilderException extends \Soosyze\Queryflatfile\Exception\Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Field.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @phpstan-type FieldToArray array{ 22 | * _comment?: string, 23 | * default?: null|scalar, 24 | * length?: int, 25 | * nullable?: bool, 26 | * opt?: string, 27 | * type: string, 28 | * unsigned?: bool, 29 | * } 30 | */ 31 | abstract class Field 32 | { 33 | public const OPT_CREATE = 'create'; 34 | 35 | public const OPT_DROP = 'drop'; 36 | 37 | public const OPT_MODIFY = 'modify'; 38 | 39 | public const OPT_RENAME = 'rename'; 40 | 41 | public const TYPE = ''; 42 | 43 | protected const INVALID_ARGUMENT_MESSAGE = 'The value of the %s field must be of type %s: %s given.'; 44 | 45 | /** 46 | * @var null|scalar 47 | */ 48 | protected $valueDefault; 49 | 50 | /** 51 | * @var string 52 | */ 53 | protected $name; 54 | 55 | /** 56 | * @var string 57 | */ 58 | protected $opt = self::OPT_CREATE; 59 | 60 | /** 61 | * @var null|string 62 | */ 63 | protected $comment = null; 64 | 65 | /** 66 | * @var bool 67 | */ 68 | protected $isNullable = false; 69 | 70 | public function __construct(string $name) 71 | { 72 | $this->name = $name; 73 | } 74 | 75 | /** 76 | * Enregistre un commentaire. 77 | * 78 | * @param string $comment 79 | * 80 | * @return $this 81 | */ 82 | public function comment(string $comment): self 83 | { 84 | $this->comment = $comment; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Enregistre le champ comme acceptant la valeur NULL. 91 | * 92 | * @return $this 93 | */ 94 | public function nullable(): self 95 | { 96 | $this->isNullable = true; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Enregistre une valeur par défaut au champ précédent. 103 | * Lève une exception si la valeur par défaut ne correspond pas au type de valeur passée en paramètre. 104 | * 105 | * @param null|scalar $value Valeur à tester. 106 | * 107 | * @throws ColumnsValueException 108 | * 109 | * @return null|scalar 110 | */ 111 | abstract public function filterValue($value); 112 | 113 | /** 114 | * Enregistre une valeur par défaut au champ précédent. 115 | * Lève une exception si la valeur par défaut ne correspond pas au type de valeur passée en paramètre. 116 | * 117 | * @param null|scalar $value Valeur à tester. 118 | * 119 | * @throws TableBuilderException 120 | * 121 | * @return $this 122 | */ 123 | public function valueDefault($value) 124 | { 125 | $this->valueDefault = $this->filterValue($value); 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * Retourne la valeur par defaut. 132 | * 133 | * @throws ColumnsValueException 134 | * 135 | * @return null|scalar Valeur par defaut. 136 | */ 137 | public function getValueDefault() 138 | { 139 | if ($this->valueDefault !== null) { 140 | return $this->valueDefault; 141 | } 142 | if ($this->isNullable) { 143 | return null; 144 | } 145 | 146 | throw new ColumnsValueException( 147 | sprintf('%s not nullable or not default.', $this->name) 148 | ); 149 | } 150 | 151 | /** 152 | * Enregistre la modification du champ précédent. 153 | * 154 | * @return void 155 | */ 156 | public function modify(): void 157 | { 158 | $this->opt = self::OPT_MODIFY; 159 | } 160 | 161 | /** 162 | * Retourne le nom de l'opération du champ. 163 | * 164 | * @return string 165 | */ 166 | public function getOpt(): string 167 | { 168 | return $this->opt; 169 | } 170 | 171 | /** 172 | * Retourne le nom du champ. 173 | * 174 | * @return string 175 | */ 176 | public function getName(): string 177 | { 178 | return $this->name; 179 | } 180 | 181 | public function setName(string $name): self 182 | { 183 | $this->name = $name; 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Retourne les données du champ. 190 | * 191 | * @return array 192 | * 193 | * @phpstan-return FieldToArray 194 | */ 195 | public function toArray(): array 196 | { 197 | $data[ 'type' ] = static::TYPE; 198 | 199 | if ($this->isNullable) { 200 | $data[ 'nullable' ] = $this->isNullable; 201 | } 202 | if ($this->comment !== null) { 203 | $data[ '_comment' ] = $this->comment; 204 | } 205 | if ($this->valueDefault !== null) { 206 | $data[ 'default' ] = $this->valueDefault; 207 | } 208 | 209 | return $data; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Field/BoolType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class BoolType extends Field 19 | { 20 | public const TYPE = 'boolean'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | * 25 | * return bool 26 | */ 27 | public function filterValue($value) 28 | { 29 | if (!\is_bool($value)) { 30 | throw new \InvalidArgumentException( 31 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, self::TYPE, gettype($value)) 32 | ); 33 | } 34 | 35 | return $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Field/CharType.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class CharType extends StringType 17 | { 18 | public const TYPE = 'char'; 19 | 20 | /** 21 | * @var int 22 | */ 23 | protected $length = 1; 24 | } 25 | -------------------------------------------------------------------------------- /src/Field/DateTimeType.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class DateTimeType extends Field 20 | { 21 | public const CURRENT_DEFAULT = 'current_datetime'; 22 | 23 | public const TYPE = 'datetime'; 24 | 25 | protected const FORMAT = 'Y-m-d H:i:s'; 26 | 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * return string 31 | */ 32 | public function filterValue($value) 33 | { 34 | if (!\is_string($value)) { 35 | throw new \InvalidArgumentException( 36 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, 'string', gettype($value)) 37 | ); 38 | } 39 | if (strtolower($value) === static::CURRENT_DEFAULT) { 40 | return static::CURRENT_DEFAULT; 41 | } 42 | if (($timestamp = strtotime($value))) { 43 | return date(static::FORMAT, $timestamp); 44 | } 45 | 46 | throw new ColumnsValueException( 47 | sprintf('The value of the %s field must be a valid date: %s given', $this->name, $value) 48 | ); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getValueDefault() 55 | { 56 | if ($this->valueDefault !== null) { 57 | if ($this->valueDefault === static::CURRENT_DEFAULT) { 58 | return date(static::FORMAT, time()); 59 | } 60 | 61 | /* Si les variables magiques ne sont pas utilisé alors la vrais valeur par defaut est retourné. */ 62 | return $this->valueDefault; 63 | } 64 | if ($this->isNullable) { 65 | return null; 66 | } 67 | 68 | throw new ColumnsValueException( 69 | sprintf('%s not nullable or not default.', $this->name) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Field/DateType.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DateType extends DateTimeType 17 | { 18 | public const CURRENT_DEFAULT = 'current_date'; 19 | 20 | public const TYPE = 'date'; 21 | 22 | protected const FORMAT = 'Y-m-d'; 23 | } 24 | -------------------------------------------------------------------------------- /src/Field/DropType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DropType extends Field 19 | { 20 | protected $opt = self::OPT_DROP; 21 | 22 | public function filterValue($value) 23 | { 24 | return null; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function toArray(): array 31 | { 32 | $data = parent::toArray(); 33 | $data[ 'opt' ] = $this->opt; 34 | 35 | return $data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Field/FloatType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class FloatType extends Field 19 | { 20 | public const TYPE = 'float'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | * 25 | * return float 26 | */ 27 | public function filterValue($value) 28 | { 29 | if (!\is_float($value)) { 30 | throw new \InvalidArgumentException( 31 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, self::TYPE, gettype($value)) 32 | ); 33 | } 34 | 35 | return (float) $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Field/IncrementType.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class IncrementType extends Field 20 | { 21 | public const TYPE = 'increments'; 22 | 23 | /** 24 | * {@inheritdoc} 25 | * 26 | * return int 27 | */ 28 | public function filterValue($value) 29 | { 30 | if (!\is_int($value)) { 31 | throw new \InvalidArgumentException( 32 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, 'integer', gettype($value)) 33 | ); 34 | } 35 | 36 | return (int) $value; 37 | } 38 | 39 | /** 40 | * @throws ColumnsValueException 41 | */ 42 | public function getValueDefault() 43 | { 44 | throw new ColumnsValueException('An incremental type column can not have a default value.'); 45 | } 46 | 47 | /** 48 | * @throws ColumnsValueException 49 | */ 50 | public function valueDefault($value) 51 | { 52 | throw new ColumnsValueException('An incremental type column can not have a default value.'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Field/IntType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class IntType extends Field 19 | { 20 | public const TYPE = 'integer'; 21 | 22 | /** 23 | * @var bool|null 24 | */ 25 | private $isUnsigned = false; 26 | 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * return int 31 | */ 32 | public function filterValue($value) 33 | { 34 | if (!\is_int($value)) { 35 | throw new \InvalidArgumentException( 36 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, self::TYPE, gettype($value)) 37 | ); 38 | } 39 | 40 | return (int) $value; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function toArray(): array 47 | { 48 | $data = parent::toArray(); 49 | 50 | if ($this->isUnsigned) { 51 | $data[ 'unsigned' ] = $this->isUnsigned; 52 | } 53 | 54 | return $data; 55 | } 56 | 57 | /** 58 | * Enregistre le champ (uniquement de type integer) comme étant non signié. 59 | * 60 | * @return $this 61 | */ 62 | public function unsigned(): self 63 | { 64 | $this->isUnsigned = true; 65 | 66 | return $this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Field/RenameType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RenameType extends Field 19 | { 20 | protected $opt = self::OPT_RENAME; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $to; 26 | 27 | public function __construct(string $name, string $to) 28 | { 29 | parent::__construct($name); 30 | $this->to = $to; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function filterValue($value) 37 | { 38 | return null; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getTo(): string 45 | { 46 | return $this->to; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function toArray(): array 53 | { 54 | $data = parent::toArray(); 55 | $data[ 'opt' ] = $this->opt; 56 | $data[ 'to' ] = $this->to; 57 | 58 | return $data; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Field/StringType.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class StringType extends TextType 17 | { 18 | public const TYPE = 'string'; 19 | 20 | /** 21 | * @var int 22 | */ 23 | protected $length = 255; 24 | 25 | public function __construct(string $name, int $length) 26 | { 27 | if ($length < 0) { 28 | throw new \InvalidArgumentException('The length passed in parameter is not of numeric type.'); 29 | } 30 | parent::__construct($name); 31 | $this->length = $length; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | * 37 | * return string 38 | */ 39 | public function filterValue($value) 40 | { 41 | /** @var string $str */ 42 | $str = parent::filterValue($value); 43 | 44 | if (strlen($str) > $this->length) { 45 | throw new \LengthException( 46 | sprintf( 47 | 'The value of the %s field must be less than or equal to %s characters: %s given', 48 | $this->name, 49 | $this->length, 50 | strlen($str) 51 | ) 52 | ); 53 | } 54 | 55 | return $value; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function toArray(): array 62 | { 63 | $data = parent::toArray(); 64 | $data[ 'length' ] = $this->length; 65 | 66 | return $data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Field/TextType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class TextType extends Field 19 | { 20 | public const TYPE = 'text'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | * 25 | * return string 26 | */ 27 | public function filterValue($value) 28 | { 29 | if (!\is_string($value)) { 30 | throw new \InvalidArgumentException( 31 | sprintf(self::INVALID_ARGUMENT_MESSAGE, $this->name, 'string', gettype($value)) 32 | ); 33 | } 34 | 35 | return $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | * @phpstan-import-type RowData from Schema 27 | * @phpstan-import-type TableData from Schema 28 | */ 29 | class Request extends RequestHandler 30 | { 31 | use ValueToString; 32 | 33 | /** 34 | * Tous les champs utilisés. 35 | * 36 | * @var Field[] 37 | */ 38 | private $allFieldsSchema; 39 | 40 | /** 41 | * Les données de la table. 42 | * 43 | * @var array 44 | * 45 | * @phpstan-var TableData 46 | */ 47 | private $tableData = []; 48 | 49 | /** 50 | * Le schéma des tables utilisées par la requête. 51 | * 52 | * @var Table 53 | */ 54 | private $table; 55 | 56 | /** 57 | * Le schéma de base de données. 58 | * 59 | * @var Schema 60 | */ 61 | private $schema; 62 | 63 | /** 64 | * Réalise une requête sur un schéma de données 65 | * 66 | * @param Schema $schema 67 | */ 68 | public function __construct(Schema $schema) 69 | { 70 | $this->schema = $schema; 71 | } 72 | 73 | /** 74 | * Retourne les paramètre de la requête en format pseudo SQL. 75 | * 76 | * @return string 77 | */ 78 | public function __toString(): string 79 | { 80 | if ($this->execute === self::INSERT) { 81 | return $this->insertIntoToString(); 82 | } 83 | if ($this->execute === self::UPDATE) { 84 | return $this->updateToString(); 85 | } 86 | if ($this->execute === self::DELETE) { 87 | return $this->deleteToString(); 88 | } 89 | 90 | $output = sprintf('SELECT %s ', $this->columnNames ? addslashes(implode(', ', $this->columnNames)) : '*'); 91 | if ($this->from !== '') { 92 | $output .= sprintf('FROM %s ', addslashes($this->from)); 93 | } 94 | foreach ($this->joins as $value) { 95 | $output .= sprintf( 96 | '%s JOIN %s ON %s ', 97 | strtoupper($value[ 'type' ]), 98 | addslashes($value[ 'table' ]), 99 | (string) $value[ 'where' ] 100 | ); 101 | } 102 | $output .= $this->whereToString(); 103 | foreach ($this->unions as $union) { 104 | $type = $union[ 'type' ] === self::UNION_SIMPLE ? 'UNION' : 'UNION ALL'; 105 | $output .= sprintf('%s %s ', $type, trim((string) $union[ 'request' ], ';')); 106 | } 107 | if ($this->orderBy) { 108 | $output .= 'ORDER BY '; 109 | foreach ($this->orderBy as $field => $order) { 110 | $output .= sprintf( 111 | '%s %s, ', 112 | addslashes($field), 113 | $order === SORT_ASC ? 'ASC' : 'DESC' 114 | ); 115 | } 116 | $output = trim($output, ', ') . ' '; 117 | } 118 | if ($this->limit !== 0) { 119 | $output .= sprintf('LIMIT %d ', (string) $this->limit); 120 | } 121 | if ($this->offset !== 0) { 122 | $output .= sprintf('OFFSET %d ', (string) $this->offset); 123 | } 124 | 125 | return trim($output) . ';'; 126 | } 127 | 128 | /** 129 | * Ajoute un schéma de données à notre requête. 130 | * 131 | * @param Schema $schema 132 | * 133 | * @return $this 134 | */ 135 | public function setSchema(Schema $schema): self 136 | { 137 | $this->schema = $schema; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Lit les données d'une table. 144 | * 145 | * @param string $tableName Nom de la table. 146 | * 147 | * @return array Données de la table. 148 | */ 149 | public function getTableData(string $tableName): array 150 | { 151 | return $this->schema->read($tableName); 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function from(string $tableName): self 158 | { 159 | parent::from($tableName); 160 | $this->table = $this->schema->getTableSchema($tableName); 161 | $this->tableData = $this->getTableData($tableName); 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Lance l'exécution d'une requête de création, modification ou suppression. 168 | * 169 | * @throws BadFunctionException 170 | */ 171 | public function execute(): void 172 | { 173 | $this->filterFrom(); 174 | $this->loadAllFieldsSchema(); 175 | $this->filterSelect(); 176 | $this->filterWhere(); 177 | 178 | if ($this->execute === self::INSERT) { 179 | $this->executeInsert(); 180 | } elseif ($this->execute === self::UPDATE) { 181 | $this->executeUpdate(); 182 | } elseif ($this->execute === self::DELETE) { 183 | $this->executeDelete(); 184 | } else { 185 | throw new BadFunctionException('Only the insert, update and delete functions can be executed.'); 186 | } 187 | 188 | $this->schema->save($this->from, $this->tableData); 189 | $this->init(); 190 | } 191 | 192 | /** 193 | * Retourne tous les résultats de la requête. 194 | * 195 | * @return array les données 196 | * 197 | * @phpstan-return TableData 198 | */ 199 | public function fetchAll(): array 200 | { 201 | $this->filterFrom(); 202 | $this->loadAllFieldsSchema(); 203 | $this->filterSelect(); 204 | $this->filterWhere(); 205 | $this->filterUnion(); 206 | $this->filterOrderBy(); 207 | $this->filterLimit(); 208 | 209 | $return = []; 210 | /* Le pointeur en cas de limite de résultat. */ 211 | $i = 0; 212 | 213 | $columnNameKeys = array_flip($this->columnNames); 214 | 215 | /* 216 | * Exécution des jointures. 217 | * La réunion et l'intersection des ensembles sont soumis à la loi interne * et donc commutative. 218 | */ 219 | foreach ($this->joins as $value) { 220 | $this->executeJoins($value[ 'type' ], $value[ 'table' ], $value[ 'where' ]); 221 | } 222 | 223 | $limitHandel = $this->orderBy || $this->unions; 224 | foreach ($this->tableData as $row) { 225 | /* WHERE */ 226 | if ($this->where && !$this->where->execute($row)) { 227 | continue; 228 | } 229 | 230 | /* LIMITE */ 231 | if ($this->limit && !$limitHandel) { 232 | if ($i++ < $this->offset) { 233 | continue; 234 | } 235 | if (($i++ > $this->sumLimit) && ($this->limit <= count($return))) { 236 | break; 237 | } 238 | } 239 | 240 | /* SELECT */ 241 | $return[] = $this->columnNames 242 | ? array_intersect_key($row, $columnNameKeys) 243 | : $row; 244 | } 245 | 246 | /* UNION */ 247 | foreach ($this->unions as $union) { 248 | /** 249 | * UNION ALL 250 | * Pour chaque requêtes unions, on récupère les résultats. 251 | * On merge puis on supprime les doublons. 252 | */ 253 | $return = array_merge($return, $union[ 'request' ]->fetchAll()); 254 | 255 | /* UNION */ 256 | if ($union[ 'type' ] === self::UNION_SIMPLE) { 257 | self::arrayUniqueMultidimensional($return); 258 | } 259 | } 260 | 261 | /* ORDER BY */ 262 | if ($this->orderBy) { 263 | $this->executeOrderBy($return, $this->orderBy); 264 | } 265 | 266 | /* LIMIT */ 267 | if ($this->limit && $limitHandel) { 268 | $return = array_slice($return, $this->offset, $this->limit); 269 | } 270 | 271 | $this->init(); 272 | 273 | return $return; 274 | } 275 | 276 | /** 277 | * Retourne le premier résultat de la requête. 278 | * 279 | * @return array Résultat de la requête. 280 | * 281 | * @phpstan-return ?RowData 282 | */ 283 | public function fetch(): ?array 284 | { 285 | $fetch = $this->limit(1)->fetchAll(); 286 | 287 | return $fetch !== [] 288 | ? $fetch[ 0 ] 289 | : null; 290 | } 291 | 292 | /** 293 | * Retourne les résultats de la requête sous forme de tableau simple, 294 | * composé uniquement du champ passé en paramètre ou du premier champ sélectionné. 295 | * 296 | * @param string $columnName Nom du champ. 297 | * @param string|null $key Clé des valeurs de la liste 298 | * 299 | * @throws ColumnsNotFoundException 300 | * 301 | * @return array Liste du champ passé en paramètre. 302 | */ 303 | public function lists(string $columnName, ?string $key = null): array 304 | { 305 | $data = $this->fetchAll(); 306 | 307 | return array_column($data, $columnName, $key); 308 | } 309 | 310 | /** 311 | * {@inheritdoc} 312 | */ 313 | public function init(): self 314 | { 315 | parent::init(); 316 | $this->allFieldsSchema = []; 317 | $this->execute = null; 318 | $this->tableData = []; 319 | $this->where = null; 320 | 321 | return $this; 322 | } 323 | 324 | /** 325 | * Revoie les instances uniques d'un tableau multidimensionnel. 326 | * 327 | * @param array $input Table multidimensionnelle. 328 | * 329 | * @return void 330 | */ 331 | protected static function arrayUniqueMultidimensional(array &$input): void 332 | { 333 | /* Sérialise les données du tableaux. */ 334 | $serialized = array_map('serialize', $input); 335 | 336 | /* Supprime les doublons sérialisés. */ 337 | $unique = array_unique($serialized); 338 | 339 | /* Redonne les clés au tableau */ 340 | $output = array_intersect_key($input, $unique); 341 | 342 | /* Renvoie le tableau avec ses clés ré-indexé */ 343 | $input = array_values($output); 344 | } 345 | 346 | /** 347 | * Execute les jointures. 348 | * 349 | * @param string $type 350 | * @param string $tableName 351 | * @param Where $where 352 | * 353 | * @return void 354 | */ 355 | protected function executeJoins(string $type, string $tableName, Where $where): void 356 | { 357 | $result = []; 358 | $rowTableNull = $this->getRowTableNull($tableName); 359 | $isLeftJoin = $type === self::JOIN_LEFT; 360 | $tableData = $isLeftJoin 361 | ? $this->tableData 362 | : $this->getTableData($tableName); 363 | $tableJoin = $isLeftJoin 364 | ? $this->getTableData($tableName) 365 | : $this->tableData; 366 | 367 | foreach ($tableData as $row) { 368 | /* Si les lignes se sont jointes. */ 369 | $addRow = false; 370 | /* Join les tables. */ 371 | foreach ($tableJoin as $rowJoin) { 372 | /* Vérifie les conditions. */ 373 | 374 | if ($isLeftJoin 375 | ? $where->executeJoin($row, $rowJoin) 376 | : $where->executeJoin($rowJoin, $row) 377 | ) { 378 | /* Join les lignes si la condition est bonne. */ 379 | $result[] = $rowJoin + $row; 380 | $addRow = true; 381 | } 382 | } 383 | 384 | /* 385 | * Si aucun resultat n'est trouvé alors la ligne est remplie 386 | * avec les colonnes de la table jointe avec des valeurs null. 387 | */ 388 | if (!$addRow) { 389 | $result[] = array_merge($rowTableNull, $row); 390 | } 391 | } 392 | $this->tableData = $result; 393 | unset($tableData, $tableJoin, $result); 394 | } 395 | 396 | /** 397 | * Trie le tableau en fonction des clés paramétrés. 398 | * 399 | * @param array $data Données à trier. 400 | * @param array $orderBy Clés sur lesquelles le trie s'exécute. 401 | * 402 | * @return void 403 | */ 404 | protected function executeOrderBy(array &$data, array $orderBy): void 405 | { 406 | foreach ($orderBy as &$order) { 407 | $order = $order === SORT_DESC 408 | ? -1 409 | : 1; 410 | } 411 | unset($order); 412 | 413 | usort($data, static function ($a, $b) use ($orderBy): int { 414 | $sorted = 0; 415 | 416 | foreach ($orderBy as $field => $order) { 417 | if ($a[ $field ] == $b[ $field ]) { 418 | continue; 419 | } 420 | 421 | $sorted = $a[ $field ] > $b[ $field ] 422 | ? 1 * $order 423 | : -1 * $order; 424 | 425 | if ($sorted !== 0) { 426 | break; 427 | } 428 | } 429 | 430 | /** @var int $sorted */ 431 | return $sorted; 432 | }); 433 | } 434 | 435 | /** 436 | * Exécute l'insertion de données. 437 | * 438 | * @throws ColumnsNotFoundException 439 | * 440 | * @return void 441 | */ 442 | protected function executeInsert(): void 443 | { 444 | /* Si l'une des colonnes est de type incrémental. */ 445 | $increment = $this->table->getIncrement(); 446 | /* Je charge les colonnes de mon schéma. */ 447 | $fields = $this->table->getFields(); 448 | $count = count($this->columnNames); 449 | 450 | foreach ($this->values as $values) { 451 | /* Pour chaque ligne je vérifie si le nombre de colonne correspond au nombre valeur insérée. */ 452 | try { 453 | /* Je prépare l'association clé=>valeur pour chaque ligne à insérer. */ 454 | $row = array_combine($this->columnNames, $values); 455 | 456 | /* PHP < 8 la méthode renvoie false */ 457 | if ($row === false) { 458 | throw new \Exception(); 459 | } 460 | } catch (\Throwable $e) { 461 | throw new ColumnsNotFoundException( 462 | sprintf( 463 | 'The number of fields in the selections are different: %s != %s', 464 | implode(', ', $this->columnNames), 465 | implode(', ', $values) 466 | ) 467 | ); 468 | } 469 | 470 | $data = []; 471 | foreach ($fields as $fieldName => $field) { 472 | /* Si mon champs existe dans le schema. */ 473 | if (isset($row[ $fieldName ])) { 474 | $data[ $fieldName ] = $field->filterValue($row[ $fieldName ]); 475 | /* Si le champ est de type incrémental et que sa valeur est supérieure à celui enregistrer dans le schéma. */ 476 | if ($field instanceof IncrementType && ($data[ $fieldName ] > $increment)) { 477 | $increment = $data[ $fieldName ]; 478 | } 479 | 480 | continue; 481 | } 482 | /* Si mon champ n'existe pas et qu'il de type incrémental. */ 483 | if ($field instanceof IncrementType) { 484 | ++$increment; 485 | $data[ $fieldName ] = $increment; 486 | 487 | continue; 488 | } 489 | 490 | /* Sinon on vérifie si une valeur par défaut lui est attribué. */ 491 | $data[ $fieldName ] = $field->getValueDefault(); 492 | } 493 | 494 | $this->tableData[] = $data; 495 | } 496 | /* Met à jour les valeurs incrémentales dans le schéma de la table. */ 497 | if ($increment !== null) { 498 | $this->schema->setIncrement($this->from, $increment); 499 | } 500 | } 501 | 502 | /** 503 | * Exécute le calcul de mise à jour des données. 504 | * 505 | * @return void 506 | */ 507 | protected function executeUpdate(): void 508 | { 509 | /* La variable $row est utilisé dans le test d'évaluation. */ 510 | foreach ($this->tableData as &$row) { 511 | if ($this->where && !$this->where->execute($row)) { 512 | continue; 513 | } 514 | $row = array_merge($row, $this->values[0]); 515 | } 516 | unset($row); 517 | } 518 | 519 | /** 520 | * Supprime des lignes de la table en fonction des conditions et sauvegarde la table. 521 | * 522 | * @return void 523 | */ 524 | protected function executeDelete(): void 525 | { 526 | foreach ($this->tableData as $key => $row) { 527 | if ($this->where && !$this->where->execute($row)) { 528 | continue; 529 | } 530 | unset($this->tableData[ $key ]); 531 | } 532 | $this->tableData = array_values($this->tableData); 533 | } 534 | 535 | private function insertIntoToString(): string 536 | { 537 | $output = sprintf('INSERT INTO %s ', addslashes($this->from)); 538 | 539 | if ($this->columnNames) { 540 | $output .= sprintf('(%s) VALUES%s', addslashes(implode(', ', $this->columnNames)), PHP_EOL); 541 | } 542 | $data = array_map( 543 | function ($values) { 544 | $data = array_map( 545 | function ($item) { 546 | return self::getValueToString($item); 547 | }, 548 | $values 549 | ); 550 | 551 | return sprintf('(%s)', implode(', ', $data)); 552 | }, 553 | $this->values 554 | ); 555 | $output .= implode(',' . PHP_EOL, $data); 556 | 557 | return trim($output) . ';'; 558 | } 559 | 560 | private function deleteToString(): string 561 | { 562 | $output = sprintf('DELETE %s ', addslashes($this->from)); 563 | $output .= $this->whereToString(); 564 | 565 | return trim($output) . ';'; 566 | } 567 | 568 | private function updateToString(): string 569 | { 570 | $output = sprintf('UPDATE %s SET ', addslashes($this->from)); 571 | $data = []; 572 | foreach ($this->values[ 0 ] as $key => $value) { 573 | $data[] = sprintf( 574 | '%s = %s', 575 | addslashes($key), 576 | self::getValueToString($value) 577 | ); 578 | } 579 | $output .= implode(', ', $data) . ' '; 580 | $output .= $this->whereToString(); 581 | 582 | return trim($output) . ';'; 583 | } 584 | 585 | private function whereToString(): string 586 | { 587 | return $this->where === null 588 | ? '' 589 | : sprintf('WHERE %s ', (string) $this->where); 590 | } 591 | 592 | /** 593 | * Charge les colonnes de la table courante et des tables de jointure. 594 | */ 595 | private function loadAllFieldsSchema(): void 596 | { 597 | $this->allFieldsSchema = $this->table->getFields(); 598 | 599 | foreach ($this->joins as $value) { 600 | $this->allFieldsSchema = array_merge( 601 | $this->allFieldsSchema, 602 | $this->schema->getTableSchema($value[ 'table' ])->getFields() 603 | ); 604 | } 605 | } 606 | 607 | /** 608 | * Vérifie l'existence de la table courante. 609 | * 610 | * @throws TableNotFoundException 611 | */ 612 | private function filterFrom(): void 613 | { 614 | if (empty($this->from)) { 615 | throw new TableNotFoundException(); 616 | } 617 | } 618 | 619 | /** 620 | * Vérifie pour tous les champs sélectionnées, leur l'existence à partir du schéma. 621 | */ 622 | private function filterSelect(): void 623 | { 624 | if ($this->columnNames) { 625 | $this->diffColumnNames($this->columnNames); 626 | } 627 | } 628 | 629 | /** 630 | * Vérifie que la limite est un entier positif. 631 | * 632 | * @throws QueryException 633 | */ 634 | private function filterLimit(): void 635 | { 636 | if ($this->limit < self::ALL) { 637 | throw new QueryException('The limit must be a non-zero positive integer.'); 638 | } 639 | if ($this->offset < 0) { 640 | throw new QueryException('The offset must be a non-zero positive integer.'); 641 | } 642 | } 643 | 644 | /** 645 | * Vérifie pour toutes les jointures (LEFT JOIN, RIGHT JOIN) et les clauses conditionnées (WHERE), 646 | * l'existence des champs à partir du schéma. 647 | */ 648 | private function filterWhere(): void 649 | { 650 | $columnNames = []; 651 | /* Merge les colonnes des conditions de la requête courante. */ 652 | if ($this->where !== null) { 653 | $columnNames = $this->where->getColumnNames(); 654 | } 655 | 656 | /* Merge toutes les colonnes des conditions de chaque jointure. */ 657 | foreach ($this->joins as $value) { 658 | $columnNames = array_merge($columnNames, $value[ 'where' ]->getColumnNames()); 659 | } 660 | 661 | if ($columnNames !== []) { 662 | $this->diffColumnNames($columnNames); 663 | } 664 | } 665 | 666 | /** 667 | * Vérifie pour tous les ORDER BY l'existence des champs à partir du schéma. 668 | * 669 | * @throws OperatorNotFound 670 | */ 671 | private function filterOrderBy(): void 672 | { 673 | if ($this->orderBy === []) { 674 | return; 675 | } 676 | 677 | $this->diffColumnNames(array_keys($this->orderBy)); 678 | 679 | foreach ($this->orderBy as $field => $order) { 680 | if ($order !== SORT_ASC && $order !== SORT_DESC) { 681 | throw new OperatorNotFound( 682 | sprintf('The sort type of the %s field is not valid.', $field) 683 | ); 684 | } 685 | } 686 | } 687 | 688 | /** 689 | * Vérifie la cohérence des champs dans chaque requêtes entre les UNIONS. 690 | * 691 | * @throws ColumnsNotFoundException 692 | */ 693 | private function filterUnion(): void 694 | { 695 | $count = count($this->columnNames); 696 | foreach ($this->unions as $union) { 697 | if ($count === count($union[ 'request' ]->getColumnNames())) { 698 | continue; 699 | } 700 | 701 | throw new ColumnsNotFoundException( 702 | sprintf( 703 | 'The number of fields in the selections are different: %s != %s', 704 | implode(', ', $this->columnNames), 705 | implode(', ', $union[ 'request' ]->getColumnNames()) 706 | ) 707 | ); 708 | } 709 | } 710 | 711 | /** 712 | * Déclenche une exception si l'un des champs passés en paramètre diffère 713 | * des champs disponibles dans les tables. 714 | * 715 | * @param string[] $columnNames Liste des nom des champs. 716 | * 717 | * @throws ColumnsNotFoundException 718 | */ 719 | private function diffColumnNames(array $columnNames): void 720 | { 721 | $diff = array_diff_key( 722 | array_flip($columnNames), 723 | $this->allFieldsSchema 724 | ); 725 | 726 | if ($diff !== []) { 727 | $columnsDiff = array_flip($diff); 728 | 729 | throw new ColumnsNotFoundException( 730 | sprintf( 731 | 'Column %s is absent: %s', 732 | implode(',', $columnsDiff), 733 | $this 734 | ) 735 | ); 736 | } 737 | } 738 | 739 | /** 740 | * Retourne un tableau associatif avec pour clé les champs de la table et pour valeur null. 741 | * Si des champs existent dans le schéma ils seront rajouté. Fonction utilisée 742 | * pour les jointures en cas d'absence de résultat. 743 | * 744 | * @param string $tableName Nom de la table. 745 | */ 746 | private function getRowTableNull(string $tableName): array 747 | { 748 | /* Prend les noms des champs de la table à joindre. */ 749 | $rowTableKey = $this->schema->getTableSchema($tableName)->getFieldsName(); 750 | /* Prend les noms des champs dans la requête précédente. */ 751 | if ($this->table->getFields() !== []) { 752 | $rowTableKey = array_merge($rowTableKey, $this->table->getFieldsName()); 753 | } 754 | /* Utilise les noms pour créer un tableau avec des valeurs null. */ 755 | return array_fill_keys($rowTableKey, null); 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /src/RequestHandler.php: -------------------------------------------------------------------------------- 1 | 19 | * 20 | * @method Request where(string $columnName, string $operator, null|scalar $value) Alias de la fonction de l'objet Queryflatfile\Where 21 | * @method Request notWhere(string $columnName, string $operator, null|scalar $value) Alias de la fonction de l'objet Queryflatfile\Where 22 | * @method Request orWhere(string $columnName, string $operator, null|scalar $value) Alias de la fonction de l'objet Queryflatfile\Where 23 | * @method Request orNotWhere(string $columnName, string $operator, null|scalar $value) Alias de la fonction de l'objet Queryflatfile\Where 24 | * 25 | * @method Request between(string $columnName, numeric|string $min, numeric|string $max) Alias de la fonction de l'objet Queryflatfile\Where 26 | * @method Request orBetween(string $columnName, numeric|string $min, numeric|string $max) Alias de la fonction de l'objet Queryflatfile\Where 27 | * @method Request notBetween(string $columnName, numeric|string $min, numeric|string $max) Alias de la fonction de l'objet Queryflatfile\Where 28 | * @method Request orNotBetween(string $columnName, numeric|string $min, numeric|string $max) Alias de la fonction de l'objet Queryflatfile\Where 29 | * 30 | * @method Request in(string $columnName, array $values) Alias de la fonction de l'objet Queryflatfile\Where 31 | * @method Request orIn(string $columnName, array $values) Alias de la fonction de l'objet Queryflatfile\Where 32 | * @method Request notIn(string $columnName, array $values) Alias de la fonction de l'objet Queryflatfile\Where 33 | * @method Request orNotIn(string $columnName, array $values) Alias de la fonction de l'objet Queryflatfile\Where 34 | * 35 | * @method Request isNull(string $columnName) Alias de la fonction de l'objet Queryflatfile\Where 36 | * @method Request orIsNull(string $columnName) Alias de la fonction de l'objet Queryflatfile\Where 37 | * @method Request isNotNull(string $columnName) Alias de la fonction de l'objet Queryflatfile\Where 38 | * @method Request orIsNotNull(string $columnName) Alias de la fonction de l'objet Queryflatfile\Where 39 | * 40 | * @method Request regex(string $columnName, string $pattern) Alias de la fonction de l'objet Queryflatfile\Where 41 | * @method Request orRegex(string $columnName, string $pattern) Alias de la fonction de l'objet Queryflatfile\Where 42 | * @method Request notRegex(string $columnName, string $pattern) Alias de la fonction de l'objet Queryflatfile\Where 43 | * @method Request orNotRegex(string $columnName, string $pattern) Alias de la fonction de l'objet Queryflatfile\Where 44 | * 45 | * @method Request whereGroup(\Closure $callable) Alias de la fonction de l'objet Queryflatfile\Where 46 | * @method Request notWhereGroup(\Closure $callable) Alias de la fonction de l'objet Queryflatfile\Where 47 | * @method Request orWhereGroup(\Closure $callable) Alias de la fonction de l'objet Queryflatfile\Where 48 | * @method Request orNotWhereGroup(\Closure $callable) Alias de la fonction de l'objet Queryflatfile\Where 49 | * 50 | * @phpstan-import-type TableData from Schema 51 | * 52 | * @phpstan-type Join array{type: string, table: string, where: Where} 53 | * @phpstan-type Union array{request: RequestInterface, type: string} 54 | */ 55 | abstract class RequestHandler implements RequestInterface 56 | { 57 | protected const INSERT = 'insert'; 58 | 59 | protected const UPDATE = 'update'; 60 | 61 | protected const DELETE = 'delete'; 62 | 63 | /** 64 | * La valeur pour une union simple. 65 | */ 66 | protected const UNION_SIMPLE = 'simple'; 67 | 68 | /** 69 | * La valeur pour une union totale. 70 | */ 71 | protected const UNION_ALL = 'all'; 72 | 73 | /** 74 | * Le type d'exécution (delete|update|insert). 75 | * 76 | * @var string|null 77 | */ 78 | protected $execute = null; 79 | 80 | /** 81 | * Le nom de la table courante. 82 | * 83 | * @var string 84 | */ 85 | protected $from = ''; 86 | 87 | /** 88 | * Les jointures à calculer. 89 | * 90 | * @var array 91 | * 92 | * @phpstan-var Join[] 93 | */ 94 | protected $joins = []; 95 | 96 | /** 97 | * Les unions. 98 | * 99 | * @var array 100 | * 101 | * @phpstan-var Union[] 102 | */ 103 | protected $unions = []; 104 | 105 | /** 106 | * Les colonnes à trier. 107 | * 108 | * @var array 109 | */ 110 | protected $orderBy = []; 111 | 112 | /** 113 | * Le nombre de résultat de la requête. 114 | * 115 | * @var int 116 | */ 117 | protected $limit = self::ALL; 118 | 119 | /** 120 | * Le décalage des résultats de la requête. 121 | * 122 | * @var int 123 | */ 124 | protected $offset = 0; 125 | 126 | /** 127 | * La somme de l'offset et de la limite. 128 | * 129 | * @var int 130 | */ 131 | protected $sumLimit = 0; 132 | 133 | /** 134 | * La liste des colonnes à mettre à jour. 135 | * 136 | * @var string[] 137 | */ 138 | protected $columnNames = []; 139 | 140 | /** 141 | * Les valeurs à insérer ou mettre à jour. 142 | * 143 | * @var array 144 | * 145 | * @phpstan-var TableData 146 | */ 147 | protected $values = []; 148 | 149 | /** 150 | * Les conditions de la requête. 151 | * 152 | * @var Where|null 153 | */ 154 | protected $where = null; 155 | 156 | /** 157 | * Permet d'utiliser les méthodes de l'objet \Queryflatfile\Where 158 | * et de personnaliser les closures pour certaines méthodes. 159 | * 160 | * @param string $name Nom de la méthode appelée. 161 | * @param array $args Pararètre de la méthode. 162 | * 163 | * @throws BadMethodCallException 164 | * 165 | * @return $this 166 | */ 167 | public function __call(string $name, array $args): self 168 | { 169 | $this->where = $this->where ?? new Where(); 170 | 171 | if (!method_exists($this->where, $name)) { 172 | throw new BadMethodCallException(sprintf('The %s method is missing.', $name)); 173 | } 174 | 175 | $this->where->$name(...$args); 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | * 183 | * @return $this 184 | */ 185 | public function delete() 186 | { 187 | $this->execute = self::DELETE; 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * {@inheritdoc} 194 | */ 195 | public function from(string $tableName) 196 | { 197 | $this->from = $tableName; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * {@inheritdoc} 204 | */ 205 | public function getColumnNames(): array 206 | { 207 | return $this->columnNames; 208 | } 209 | 210 | /** 211 | * {@inheritdoc} 212 | */ 213 | public function insertInto(string $tableName, array $columnNames) 214 | { 215 | $this->execute = self::INSERT; 216 | $this->from($tableName)->select(...$columnNames); 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * {@inheritdoc} 223 | */ 224 | public function leftJoin(string $tableName, $column, string $operator = '', string $value ='') 225 | { 226 | if ($column instanceof \Closure) { 227 | $this->joinGroup(self::JOIN_LEFT, $tableName, $column); 228 | 229 | return $this; 230 | } 231 | $this->join(self::JOIN_LEFT, $tableName, $column, $operator, $value); 232 | 233 | return $this; 234 | } 235 | 236 | /** 237 | * {@inheritdoc} 238 | */ 239 | public function limit(int $limit, int $offset = 0) 240 | { 241 | $this->limit = $limit; 242 | $this->offset = $offset; 243 | $this->sumLimit = $offset + $limit; 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * {@inheritdoc} 250 | */ 251 | public function orderBy(string $columnName, int $order = SORT_ASC) 252 | { 253 | $this->orderBy[ $columnName ] = $order; 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * {@inheritdoc} 260 | */ 261 | public function rightJoin(string $tableName, $column, string $operator = '', string $value = '') 262 | { 263 | if ($column instanceof \Closure) { 264 | $this->joinGroup(self::JOIN_RIGHT, $tableName, $column); 265 | 266 | return $this; 267 | } 268 | $this->join(self::JOIN_RIGHT, $tableName, $column, $operator, $value); 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * {@inheritdoc} 275 | */ 276 | public function select(string ...$columnNames) 277 | { 278 | $this->columnNames = $columnNames; 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * {@inheritdoc} 285 | */ 286 | public function union(RequestInterface $request) 287 | { 288 | $this->unions[] = [ 'request' => $request, 'type' => self::UNION_SIMPLE ]; 289 | 290 | return $this; 291 | } 292 | 293 | /** 294 | * {@inheritdoc} 295 | */ 296 | public function unionAll(RequestInterface $request) 297 | { 298 | $this->unions[] = [ 'request' => $request, 'type' => self::UNION_ALL ]; 299 | 300 | return $this; 301 | } 302 | 303 | /** 304 | * {@inheritdoc} 305 | */ 306 | public function update(string $tableName, array $row) 307 | { 308 | $this->execute = self::UPDATE; 309 | $this->from($tableName)->select(...array_keys($row)); 310 | $this->values[ 0 ] = $row; 311 | 312 | return $this; 313 | } 314 | 315 | /** 316 | * {@inheritdoc} 317 | */ 318 | public function values(array $rowValues) 319 | { 320 | $this->values[] = $rowValues; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * Initialise les paramètre de la requête. 327 | * 328 | * @return $this 329 | */ 330 | protected function init() 331 | { 332 | $this->columnNames = []; 333 | $this->execute = null; 334 | $this->from = ''; 335 | $this->joins = []; 336 | $this->limit = self::ALL; 337 | $this->offset = 0; 338 | $this->orderBy = []; 339 | $this->sumLimit = 0; 340 | $this->unions = []; 341 | $this->values = []; 342 | 343 | return $this; 344 | } 345 | 346 | /** 347 | * Enregistre une jointure. 348 | * 349 | * @param string $type Type de la jointure. 350 | * @param string $tableName Nom de la table à joindre 351 | * @param string $columnName Nom de la colonne d'une des tables précédentes. 352 | * @param string $operator Opérateur logique ou null pour une closure. 353 | * @param string $value Valeur ou une colonne de la table jointe (au format nom_table.colonne) 354 | */ 355 | private function join(string $type, string $tableName, string $columnName, string $operator, string $value): void 356 | { 357 | $where = new Where(); 358 | $where->where($columnName, $operator, $value); 359 | 360 | $this->joins[] = [ 'type' => $type, 'table' => $tableName, 'where' => $where ]; 361 | } 362 | 363 | /** 364 | * Enregistre une jointure avec une condition groupée. 365 | * 366 | * @param string $type Type de la jointure. 367 | * @param string $tableName Nom de la table à joindre 368 | * @param \Closure $callable Nom de la colonne d'une des tables précédentes. 369 | */ 370 | private function joinGroup(string $type, string $tableName, \Closure $callable): void 371 | { 372 | $where = new Where(); 373 | call_user_func_array($callable, [ &$where ]); 374 | 375 | $this->joins[] = [ 'type' => $type, 'table' => $tableName, 'where' => $where ]; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/RequestInterface.php: -------------------------------------------------------------------------------- 1 | 20 | 21 | * @phpstan-import-type RowValues from Schema 22 | * @phpstan-import-type RowData from Schema 23 | * @phpstan-import-type TableData from Schema 24 | */ 25 | interface RequestInterface 26 | { 27 | /** 28 | * La valeur par défaut de LIMIT. 29 | */ 30 | public const ALL = 0; 31 | 32 | /** 33 | * Valeur pour un join gauche. 34 | */ 35 | public const JOIN_LEFT = 'left'; 36 | 37 | /** 38 | * Valeur pour un join droit. 39 | */ 40 | public const JOIN_RIGHT = 'right'; 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function __toString(): string; 46 | 47 | /** 48 | * Enregistre les champs sélectionnées par la requête. 49 | * En cas d'absence de selection, la requêtes retournera toutes les champs. 50 | * 51 | * @param string ...$columnNames Liste ou tableau des noms des colonnes. 52 | * 53 | * @return $this 54 | */ 55 | public function select(string ...$columnNames); 56 | 57 | /** 58 | * @return string[] 59 | */ 60 | public function getColumnNames(): array; 61 | 62 | /** 63 | * Enregistre le nom de la source des données principale de la requête. 64 | * 65 | * @param string $tableName Nom de la table. 66 | * 67 | * @return $this 68 | */ 69 | public function from(string $tableName); 70 | 71 | /** 72 | * Enregistre une jointure gauche. 73 | * 74 | * @param string $tableName Nom de la table à joindre. 75 | * @param string|\Closure $column Nom de la colonne d'une des tables précédentes ou un group de condition 76 | * ou une closure pour affiner les conditions. 77 | * @param string $operator Opérateur logique ou null pour une closure. 78 | * @param string $value Colonne de la table jointe (au format nom_table.colonne) 79 | * 80 | * @return $this 81 | */ 82 | public function leftJoin(string $tableName, $column, string $operator = '', string $value = ''); 83 | 84 | /** 85 | * Enregistre une jointure droite. 86 | * 87 | * @param string $tableName Nom de la table à joindre 88 | * @param string|\Closure $column Nom de la colonne d'une des tables précédentes ou un group de condition 89 | * ou une closure pour affiner les conditions. 90 | * @param string $operator Opérateur logique ou null pour une closure. 91 | * @param string $value Colonne de la table jointe (au format nom_table.colonne) 92 | * 93 | * @return $this 94 | */ 95 | public function rightJoin(string $tableName, $column, string $operator = '', string $value = ''); 96 | 97 | /** 98 | * Enregistre une limitation et un décalage au retour de la requête. 99 | * 100 | * @param int $limit Nombre de résultat maximum à retourner. 101 | * @param int $offset Décalage sur le jeu de résultat. 102 | * 103 | * @return $this 104 | */ 105 | public function limit(int $limit, int $offset = 0); 106 | 107 | /** 108 | * Enregistre un trie des résultats de la requête. 109 | * 110 | * @param string $columnName Nom de la colonne à trier. 111 | * @param int $order Ordre du trie (SORT_ASC|SORT_DESC). 112 | * 113 | * @return $this 114 | */ 115 | public function orderBy(string $columnName, int $order = SORT_ASC); 116 | 117 | /** 118 | * Enregistre l'action d'insertion de données. 119 | * Cette fonction doit-être suivie la fonction values(). 120 | * 121 | * @param string $tableName Nom de la table. 122 | * @param string[] $columnNames Liste des champs par ordre d'insertion dans 123 | * la fonction values(). 124 | * 125 | * @return $this 126 | */ 127 | public function insertInto(string $tableName, array $columnNames); 128 | 129 | /** 130 | * Cette fonction doit suivre la fonction insertInto(). 131 | * Les valeurs doivent suivre le même ordre que les clés précédemment enregistrées. 132 | * 133 | * @param array $rowValues Valeurs des champs. 134 | * 135 | * @phpstan-param RowValues $rowValues 136 | * 137 | * @return $this 138 | */ 139 | public function values(array $rowValues); 140 | 141 | /** 142 | * Enregistre l'action de modification de données. 143 | * 144 | * @param string $tableName Nom de la table. 145 | * @param array $row key=>value des données à modifier. 146 | * 147 | * @phpstan-param RowData $row 148 | * 149 | * @return $this 150 | */ 151 | public function update(string $tableName, array $row); 152 | 153 | /** 154 | * Enregistre l'action de suppression des données. 155 | * 156 | * @return $this 157 | */ 158 | public function delete(); 159 | 160 | /** 161 | * Enregistre une union 'simple' entre 2 ensembles. 162 | * Le résultat de l'union ne possède pas de doublon de ligne. 163 | * 164 | * @param RequestInterface $request Seconde requête. 165 | * 166 | * @return $this 167 | */ 168 | public function union(RequestInterface $request); 169 | 170 | /** 171 | * Enregistre une union all entre 2 ensembles. 172 | * Les doublons de lignes figure dans le resultat de l'union. 173 | * 174 | * @param RequestInterface $request 175 | * 176 | * @return $this 177 | */ 178 | public function unionAll(RequestInterface $request); 179 | 180 | /** 181 | * Retourne tous les résultats de la requête. 182 | * 183 | * @return array les données 184 | * 185 | * @phpstan-return TableData 186 | */ 187 | public function fetchAll(): array; 188 | 189 | /** 190 | * Retourne le premier résultat de la requête. 191 | * 192 | * @return array Résultat de la requête. 193 | * 194 | * @phpstan-return ?RowData 195 | */ 196 | public function fetch(): ?array; 197 | 198 | /** 199 | * Retourne les résultats de la requête sous forme de tableau simple, 200 | * composé uniquement du champ passé en paramètre ou du premier champ sélectionné. 201 | * 202 | * @param string $name Nom du champ. 203 | * @param string|null $key Clé des valeurs de la liste 204 | * 205 | * @throws ColumnsNotFoundException 206 | * 207 | * @return array Liste du champ passé en paramètre. 208 | */ 209 | public function lists(string $name, ?string $key = null): array; 210 | 211 | /** 212 | * Lance l'exécution d'une requête de création, modification ou suppression. 213 | * 214 | * @throws BadFunctionException 215 | */ 216 | public function execute(): void; 217 | } 218 | -------------------------------------------------------------------------------- /src/Schema.php: -------------------------------------------------------------------------------- 1 | 28 | * 29 | * @phpstan-type RowData array 30 | * @phpstan-type RowValues array 31 | * @phpstan-type TableData RowData[] 32 | */ 33 | class Schema 34 | { 35 | /** 36 | * Format de la base de données. 37 | * 38 | * @var DriverInterface 39 | */ 40 | protected $driver; 41 | 42 | /** 43 | * Répertoire de stockage. 44 | * 45 | * @var string 46 | */ 47 | protected $path; 48 | 49 | /** 50 | * La racine pour le répertoire de stockage. 51 | * 52 | * @var string 53 | */ 54 | protected $root = ''; 55 | 56 | /** 57 | * Nom du schéma. 58 | * 59 | * @var string 60 | */ 61 | protected $name; 62 | 63 | /** 64 | * Schéma des tables. 65 | * 66 | * @var array 67 | */ 68 | protected $schema = []; 69 | 70 | /** 71 | * Construis l'objet avec une configuration. 72 | * 73 | * @param string $host Répertoire de stockage des données. 74 | * @param string $name Nom du fichier contenant le schéma de base. 75 | * @param DriverInterface $driver Interface de manipulation de données. 76 | */ 77 | public function __construct( 78 | ?string $host = null, 79 | string $name = 'schema', 80 | DriverInterface $driver = null 81 | ) { 82 | if ($host !== null) { 83 | $this->setConfig($host, $name, $driver); 84 | } 85 | } 86 | 87 | /** 88 | * Enregistre la configuration. 89 | * 90 | * @param string $host Répertoire de stockage des données. 91 | * @param string $name Nom du fichier contenant le schéma de base. 92 | * @param DriverInterface|null $driver Interface de manipulation de données. 93 | * 94 | * @return $this 95 | */ 96 | public function setConfig( 97 | string $host, 98 | string $name = 'schema', 99 | DriverInterface $driver = null 100 | ): self { 101 | $this->driver = $driver ?? new Driver\Json(); 102 | $this->path = $host; 103 | $this->name = $name; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Retourne le chemin relatif au répertoire de stockage. 110 | * 111 | * @return string 112 | */ 113 | public function getPath(): string 114 | { 115 | return $this->path; 116 | } 117 | 118 | /** 119 | * Ajoute la racine du répertoire de stockage. 120 | * 121 | * @param string $root 122 | * 123 | * @return $this 124 | */ 125 | public function setPathRoot(string $root = ''): self 126 | { 127 | $this->root = $root; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Modifie la valeur incrémentale d'une table. 134 | * 135 | * @param string $tableName Nom de la table. 136 | * @param int $increment Nouvelle valeur incrémentale. 137 | * 138 | * @throws TableNotFoundException 139 | * @throws Exception 140 | * 141 | * @return bool Si le schéma d'incrémentaion est bien enregistré. 142 | */ 143 | public function setIncrement(string $tableName, int $increment): bool 144 | { 145 | if (!$this->hasTable($tableName)) { 146 | throw new TableNotFoundException($tableName); 147 | } 148 | 149 | if (!$this->schema[ $tableName ]->hasIncrement()) { 150 | throw new Exception( 151 | sprintf('Table %s does not have an incremental value.', $tableName) 152 | ); 153 | } 154 | 155 | $this->schema[ $tableName ]->setIncrement($increment); 156 | 157 | return $this->save($this->name, $this->toArray()); 158 | } 159 | 160 | /** 161 | * Retourne la valeur incrémentale d'une table. 162 | * 163 | * @param string $tableName Nom de la table. 164 | * 165 | * @throws TableNotFoundException 166 | * @throws Exception 167 | * 168 | * @return int 169 | */ 170 | public function getIncrement(string $tableName): int 171 | { 172 | if (!$this->hasTable($tableName)) { 173 | throw new TableNotFoundException($tableName); 174 | } 175 | 176 | if ($this->schema[ $tableName ]->getIncrement() === null) { 177 | throw new Exception( 178 | sprintf('Table %s does not have an incremental value.', $tableName) 179 | ); 180 | } 181 | 182 | return $this->schema[ $tableName ]->getIncrement(); 183 | } 184 | 185 | /** 186 | * Retourne le schema des tables. 187 | * 188 | * @return array Schéma de la base de données. 189 | */ 190 | public function getSchema(): array 191 | { 192 | if ($this->schema) { 193 | return $this->schema; 194 | } 195 | $this->create($this->name); 196 | $schema = $this->read($this->name); 197 | 198 | /** @var string $tableName */ 199 | foreach ($schema as $tableName => $table) { 200 | $this->schema[ $tableName ] = TableBuilder::createTableFromArray($tableName, $table); 201 | } 202 | 203 | return $this->schema; 204 | } 205 | 206 | /** 207 | * Cherche le schéma de la table passée en paramètre. 208 | * 209 | * @param string $tableName Nom de la table. 210 | * 211 | * @throws TableNotFoundException 212 | * 213 | * @return Table Schéma de la table. 214 | */ 215 | public function getTableSchema(string $tableName): Table 216 | { 217 | if (!$this->hasTable($tableName)) { 218 | throw new TableNotFoundException($tableName); 219 | } 220 | 221 | return $this->getSchema()[ $tableName ]; 222 | } 223 | 224 | /** 225 | * Supprime le schéma courant des données. 226 | * 227 | * @return $this 228 | */ 229 | public function dropSchema(): self 230 | { 231 | $schema = $this->getSchema(); 232 | 233 | /* Supprime les fichiers des tables. */ 234 | foreach (array_keys($schema) as $tableName) { 235 | $this->delete($tableName); 236 | } 237 | 238 | /* Supprime le fichier de schéma. */ 239 | $this->delete($this->name); 240 | 241 | /* 242 | * Dans le cas ou le répertoire utilisé contient d'autre fichier 243 | * (Si le répertoire contient que les 2 élements '.' et '..') 244 | * alors nous le supprimons. 245 | */ 246 | $dir = scandir($this->root . $this->path); 247 | if ($dir !== false && count($dir) == 2) { 248 | rmdir($this->root . $this->path); 249 | } 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * Créer une référence dans le schéma et le fichier de la table. 256 | * 257 | * @param string $tableName Nom de la table. 258 | * @param callable|null $callback fonction(TableBuilder $tableName) pour créer les champs. 259 | * 260 | * @throws Exception 261 | * 262 | * @return $this 263 | */ 264 | public function createTable(string $tableName, ?callable $callback = null): self 265 | { 266 | if ($this->hasTable($tableName)) { 267 | throw new Exception(sprintf('Table %s exist.', $tableName)); 268 | } 269 | 270 | $this->schema[ $tableName ] = self::tableBuilder($tableName, $callback)->getTable(); 271 | 272 | $this->save($this->name, $this->toArray()); 273 | $this->create($tableName); 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * Créer une référence dans le schéma et un fichier de données si ceux si n'existe pas. 280 | * 281 | * @param string $tableName Nom de la table. 282 | * @param callable|null $callback fonction(TableBuilder $table) pour créer les champs. 283 | * 284 | * @return $this 285 | */ 286 | public function createTableIfNotExists(string $tableName, ?callable $callback = null): self 287 | { 288 | /* Créer la table si elle n'existe pas dans le schéma. */ 289 | if (!$this->hasTable($tableName)) { 290 | $this->createTable($tableName, $callback); 291 | } elseif (!$this->driver->has($this->root . $this->path, $tableName)) { 292 | /* Si elle existe dans le schéma et que le fichier est absent alors on le créer. */ 293 | $this->create($tableName); 294 | } 295 | 296 | return $this; 297 | } 298 | 299 | /** 300 | * Modifie les champs du schéma de données. 301 | * 302 | * @param string $tableName Nom de la table. 303 | * @param callable $callback fonction(TableAleter $tableAlter) pour manipuler les champs. 304 | * 305 | * @return $this 306 | */ 307 | public function alterTable(string $tableName, callable $callback): self 308 | { 309 | $tableSchema = $this->getTableSchema($tableName); 310 | $tableBuilder = self::tableAlterBuilder($tableName, $callback)->getTable(); 311 | $tableData = $this->read($tableName); 312 | 313 | foreach ($tableBuilder->getFields() as $field) { 314 | if ($field->getOpt() === Field::OPT_CREATE) { 315 | self::filterFieldAdd($tableSchema, $field); 316 | self::add($tableSchema, $field, $tableData); 317 | } elseif ($field->getOpt() === Field::OPT_RENAME) { 318 | self::filterFieldRename($tableSchema, $field); 319 | self::rename($tableSchema, $field, $tableData); 320 | } elseif ($field->getOpt() === Field::OPT_MODIFY) { 321 | self::filterFieldModify($tableSchema, $field); 322 | self::modify($tableSchema, $field, $tableData); 323 | } elseif ($field->getOpt() === Field::OPT_DROP) { 324 | self::filterFieldDrop($tableSchema, $field); 325 | self::drop($tableSchema, $field, $tableData); 326 | } 327 | } 328 | 329 | $this->schema[ $tableName ] = $tableSchema; 330 | $this->save($this->name, $this->toArray()); 331 | $this->save($tableName, $tableData); 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * Détermine une table existe. 338 | * 339 | * @param string $tableName Nom de la table. 340 | * 341 | * @return bool Si le schéma de référence et le fichier de données existent. 342 | */ 343 | public function hasTable(string $tableName): bool 344 | { 345 | return isset($this->getSchema()[ $tableName ]) && $this->driver->has($this->root . $this->path, $tableName); 346 | } 347 | 348 | /** 349 | * Détermine si une colonne existe. 350 | * 351 | * @param string $tableName Nom de la table. 352 | * @param string $columnName Nom de la colonne. 353 | * 354 | * @return bool Si le schéma de référence et le fichier de données existent. 355 | */ 356 | public function hasColumn(string $tableName, string $columnName): bool 357 | { 358 | return isset($this->getSchema()[ $tableName ]) && 359 | $this->getSchema()[ $tableName ]->hasField($columnName) && 360 | $this->driver->has($this->root . $this->path, $tableName); 361 | } 362 | 363 | /** 364 | * Vide la table et initialise les champs incrémentaux. 365 | * 366 | * @param String $tableName Nom de la table. 367 | * 368 | * @throws TableNotFoundException 369 | * 370 | * @return bool 371 | */ 372 | public function truncateTable(string $tableName): bool 373 | { 374 | if (!$this->hasTable($tableName)) { 375 | throw new TableNotFoundException($tableName); 376 | } 377 | 378 | $deleteSchema = true; 379 | if ($this->schema[ $tableName ]->hasIncrement()) { 380 | $this->schema[ $tableName ]->setIncrement(0); 381 | 382 | $deleteSchema = $this->save($this->name, $this->toArray()); 383 | } 384 | $deleteData = $this->save($tableName, []); 385 | 386 | return $deleteSchema && $deleteData; 387 | } 388 | 389 | /** 390 | * Supprime du schéma la référence de la table et son fichier de données. 391 | * 392 | * @param string $tableName Nom de la table. 393 | * 394 | * @throws TableNotFoundException 395 | * 396 | * @return bool Si la suppression du schema et des données se son bien passé. 397 | */ 398 | public function dropTable(string $tableName): bool 399 | { 400 | if (!$this->hasTable($tableName)) { 401 | throw new TableNotFoundException($tableName); 402 | } 403 | 404 | unset($this->schema[ $tableName ]); 405 | $deleteData = $this->delete($tableName); 406 | $deleteSchema = $this->save($this->name, $this->toArray()); 407 | 408 | return $deleteSchema && $deleteData; 409 | } 410 | 411 | /** 412 | * Supprime une table si elle existe. 413 | * 414 | * @param string $tableName Nom de la table. 415 | * 416 | * @return bool Si la table n'existe plus. 417 | */ 418 | public function dropTableIfExists(string $tableName): bool 419 | { 420 | return $this->hasTable($tableName) && $this->dropTable($tableName); 421 | } 422 | 423 | /** 424 | * Utilisation du driver pour connaître l'extension de fichier utilisé. 425 | * 426 | * @return string Extension de fichier sans le '.'. 427 | */ 428 | public function getExtension(): string 429 | { 430 | return $this->driver->getExtension(); 431 | } 432 | 433 | /** 434 | * Utilisation du driver pour lire un fichier. 435 | * 436 | * @param string $file 437 | * 438 | * @return array le contenu du fichier 439 | */ 440 | public function read(string $file): array 441 | { 442 | return $this->driver->read($this->root . $this->path, $file); 443 | } 444 | 445 | /** 446 | * Utilisation du driver pour enregistrer des données dans un fichier. 447 | * 448 | * @param string $file 449 | * @param array $data 450 | * 451 | * @return bool 452 | */ 453 | public function save(string $file, array $data): bool 454 | { 455 | return $this->driver->save($this->root . $this->path, $file, $data); 456 | } 457 | 458 | /** 459 | * Utilisation du driver pour créer un fichier. 460 | * 461 | * @param string $file 462 | * @param array $data 463 | * 464 | * @return bool 465 | */ 466 | protected function create(string $file, array $data = []): bool 467 | { 468 | return $this->driver->create($this->root . $this->path, $file, $data); 469 | } 470 | 471 | /** 472 | * Utilisation du driver pour supprimer un fichier. 473 | * 474 | * @param string $file 475 | * 476 | * @return bool 477 | */ 478 | protected function delete(string $file): bool 479 | { 480 | return $this->driver->delete($this->root . $this->path, $file); 481 | } 482 | 483 | /** 484 | * Ajoute un champ dans les paramètre de la table et ses données. 485 | * 486 | * @param Table $table Schéma de la table. 487 | * @param Field $field Nouveau champ. 488 | * @param array $tableData Les données de la table. 489 | * 490 | * @phpstan-param TableData $tableData 491 | * 492 | * @return void 493 | */ 494 | protected static function add( 495 | Table &$table, 496 | Field $field, 497 | array &$tableData 498 | ): void { 499 | $table->addField($field); 500 | 501 | $increment = $field instanceof IncrementType 502 | ? 0 503 | : null; 504 | 505 | try { 506 | $valueDefault = $field->getValueDefault(); 507 | } catch (ColumnsValueException | \InvalidArgumentException $e) { 508 | $valueDefault = ''; 509 | } 510 | 511 | foreach ($tableData as &$data) { 512 | $data[ $field->getName() ] = $increment === null 513 | ? $valueDefault 514 | : ++$increment; 515 | } 516 | 517 | if ($increment !== null) { 518 | $table->setIncrement($increment); 519 | } 520 | } 521 | 522 | /** 523 | * Modifie un champ dans les paramètre de la table et ses données. 524 | * 525 | * @param Table $table Schéma de la table. 526 | * @param Field $field Champ modifié. 527 | * @param array $tableData Les données de la table. 528 | * 529 | * @return void 530 | */ 531 | protected static function modify( 532 | Table &$table, 533 | Field $field, 534 | array &$tableData 535 | ): void { 536 | $oldField = $table->getField($field->getName()); 537 | 538 | $table->addField($field); 539 | 540 | /** 541 | * Si la modification ne concerne pas le type, la mise à jour des données ne se fait pas. 542 | * Exemple: rendre un champ nullable ne doit écraser les données présentent en table. 543 | */ 544 | if ($oldField::TYPE === $field::TYPE) { 545 | return; 546 | } 547 | 548 | $increment = $field instanceof IncrementType 549 | ? 0 550 | : null; 551 | 552 | try { 553 | $valueDefault = $field->getValueDefault(); 554 | foreach ($tableData as &$data) { 555 | $data[ $field->getName() ] = $valueDefault; 556 | } 557 | } catch (ColumnsValueException | \InvalidArgumentException $e) { 558 | } 559 | 560 | if ($increment !== null) { 561 | $table->setIncrement($increment); 562 | } 563 | } 564 | 565 | /** 566 | * Renomme un champ dans les paramètre de la table et ses données. 567 | * 568 | * @param Table $table Schéma de la table. 569 | * @param RenameType $fieldRename champ à renommer 570 | * @param array $tableData Les données de la table. 571 | * 572 | * @return void 573 | */ 574 | protected static function rename( 575 | Table &$table, 576 | RenameType $fieldRename, 577 | array &$tableData 578 | ): void { 579 | $table->renameField($fieldRename->getName(), $fieldRename->getTo()); 580 | 581 | foreach ($tableData as &$data) { 582 | $data[ $fieldRename->getTo() ] = $data[ $fieldRename->getName() ]; 583 | unset($data[ $fieldRename->getName() ]); 584 | } 585 | } 586 | 587 | /** 588 | * Supprime un champ dans les paramètre de la table et ses données. 589 | * 590 | * @param Table $table Schéma de la table. 591 | * @param DropType $fieldDrop Champ à supprimer 592 | * @param array $tableData Les données de la table. 593 | * 594 | * @return void 595 | */ 596 | protected static function drop( 597 | Table &$table, 598 | DropType $fieldDrop, 599 | array &$tableData 600 | ): void { 601 | foreach (array_keys($tableData) as $key) { 602 | unset($tableData[ $key ][ $fieldDrop->getName() ]); 603 | } 604 | 605 | if ($table->getField($fieldDrop->getName()) instanceof IncrementType) { 606 | $table->setIncrement(null); 607 | } 608 | $table->unsetField($fieldDrop->getName()); 609 | } 610 | 611 | /** 612 | * Passe en premier paramètre d'une fonction anonyme un objet TableBuilder et le retourne. 613 | * 614 | * @param string $tableName Nom de la table. 615 | * @param callable $callback Fonction anonyme. 616 | * 617 | * @return TableBuilder 618 | */ 619 | protected static function tableAlterBuilder(string $tableName, callable $callback): TableBuilder 620 | { 621 | $builder = new TableAlter($tableName); 622 | call_user_func_array($callback, [ &$builder ]); 623 | 624 | return $builder; 625 | } 626 | 627 | /** 628 | * Passe en premier paramètre d'une fonction anonyme un objet TableBuilder et le retourne. 629 | * 630 | * @param string $tableName Nom de la table. 631 | * @param callable|null $callback Fonction anonyme. 632 | * 633 | * @return TableBuilder 634 | */ 635 | protected static function tableBuilder(string $tableName, ?callable $callback = null): TableBuilder 636 | { 637 | $builder = new TableBuilder($tableName); 638 | if ($callback !== null) { 639 | call_user_func_array($callback, [ &$builder ]); 640 | } 641 | 642 | return $builder; 643 | } 644 | 645 | /** 646 | * Vérifie si les opérations d'ajout du champ sont conformes. 647 | * 648 | * @param Table $tableSchema Le schéma de la table. 649 | * @param Field $field Nouveau champ. 650 | * 651 | * @throws Exception 652 | * @throws ColumnsNotFoundException 653 | */ 654 | private static function filterFieldAdd(Table $tableSchema, Field $field): void 655 | { 656 | /* Si un champ est ajouté il ne doit pas exister dans le schéma. */ 657 | if ($tableSchema->hasField($field->getName())) { 658 | throw new Exception( 659 | sprintf( 660 | '%s field does not exists in %s table.', 661 | $field->getName(), 662 | $tableSchema->getName() 663 | ) 664 | ); 665 | } 666 | if ($tableSchema->hasIncrement() && $field instanceof IncrementType) { 667 | throw new ColumnsValueException( 668 | sprintf( 669 | 'The %s table can not have multiple incremental values.', 670 | $tableSchema->getName() 671 | ) 672 | ); 673 | } 674 | } 675 | 676 | /** 677 | * Vérifie si les opérations de modification du champ sont conformes. 678 | * 679 | * @param Table $tableSchema Le schéma de la table. 680 | * @param Field $field Champ à modifier. 681 | * 682 | * @throws ColumnsNotFoundException 683 | * @throws ColumnsValueException 684 | * @throws Exception 685 | */ 686 | private static function filterFieldModify(Table $tableSchema, Field $field): void 687 | { 688 | if (!$tableSchema->hasField($field->getName())) { 689 | throw new Exception( 690 | sprintf( 691 | '%s field does not exists in %s table.', 692 | $field->getName(), 693 | $tableSchema->getName() 694 | ) 695 | ); 696 | } 697 | if ($tableSchema->hasIncrement() && $field instanceof IncrementType) { 698 | throw new ColumnsValueException( 699 | sprintf( 700 | 'The %s table can not have multiple incremental values.', 701 | $tableSchema->getName() 702 | ) 703 | ); 704 | } 705 | 706 | $fieldOld = $tableSchema->getField($field->getName()); 707 | 708 | /* Si le type change, les données présents doivent être d'un type équivalent. */ 709 | $modifyNumber = in_array($field::TYPE, [ 'integer', 'float', 'increments' ]) && 710 | !in_array($fieldOld::TYPE, [ 'integer', 'float', 'increments' ]); 711 | $modifyString = in_array($field::TYPE, [ 'text', 'string', 'char' ]) && 712 | !in_array($fieldOld::TYPE, [ 'text', 'string', 'char' ]); 713 | $modifyDate = in_array($field::TYPE, [ 'date', 'datetime' ]) && 714 | !in_array($fieldOld::TYPE, [ 'date', 'datetime', 'string', 'text' ]); 715 | 716 | if ($modifyString || $modifyNumber || $modifyDate) { 717 | throw new Exception( 718 | sprintf( 719 | 'The %s column type %s can not be changed with the %s type.', 720 | $field->getName(), 721 | $fieldOld::TYPE, 722 | $field::TYPE 723 | ) 724 | ); 725 | } 726 | } 727 | 728 | /** 729 | * Vérifie si les opérations de renommage du champ sont conformes. 730 | * 731 | * @param Table $table Le schéma de la table. 732 | * @param RenameType $field Champ à renommer. 733 | * 734 | * @throws ColumnsNotFoundException 735 | * @throws Exception 736 | */ 737 | private static function filterFieldRename(Table $table, RenameType $field): void 738 | { 739 | if (!$table->hasField($field->getName())) { 740 | throw new ColumnsNotFoundException( 741 | sprintf('%s field does not exists in %s table.', $field->getName(), $table->getName()) 742 | ); 743 | } 744 | /* Si le champ à renommer existe dans le schema. */ 745 | if ($table->hasField($field->getTo())) { 746 | throw new Exception( 747 | sprintf('%s field does exists in %s table.', $field->getName(), $table->getName()) 748 | ); 749 | } 750 | } 751 | 752 | /** 753 | * Vérifie si les opérations de suppression du champ sont conformes. 754 | * 755 | * @param Table $table Le schéma de la table. 756 | * @param DropType $field Champ à supprimer 757 | * 758 | * @throws ColumnsNotFoundException 759 | * 760 | * @return void 761 | */ 762 | private static function filterFieldDrop(Table $table, DropType $field): void 763 | { 764 | if (!$table->hasField($field->getName())) { 765 | throw new ColumnsNotFoundException( 766 | sprintf('%s field does not exists in %s table.', $field->getName(), $table->getName()) 767 | ); 768 | } 769 | } 770 | 771 | private function toArray(): array 772 | { 773 | $tables = []; 774 | foreach ($this->schema as $tableName => $table) { 775 | $tables[ $tableName ] = $table->toArray(); 776 | } 777 | 778 | return $tables; 779 | } 780 | } 781 | -------------------------------------------------------------------------------- /src/Table.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @phpstan-import-type FieldToArray from Field 19 | * 20 | * @phpstan-type TableToArray array{ 21 | * fields: array, 22 | * increments?: int|null 23 | * } 24 | */ 25 | final class Table 26 | { 27 | /** 28 | * @var string 29 | */ 30 | protected $name; 31 | 32 | /** 33 | * Les champs et leurs paramètres. 34 | * 35 | * @var array 36 | */ 37 | protected $fields = []; 38 | 39 | /** 40 | * La valeur des champs incrémentaux. 41 | * 42 | * @var int|null 43 | */ 44 | private $increment = null; 45 | 46 | public function __construct(string $name) 47 | { 48 | $this->name = $name; 49 | } 50 | 51 | /** 52 | * Ajoute un nouveau champ. 53 | * 54 | * @param Field $field Champ. 55 | * 56 | * @return void 57 | */ 58 | public function addField(Field $field): void 59 | { 60 | if ($field instanceof IncrementType) { 61 | $this->increment = 0; 62 | } 63 | 64 | $this->fields[ $field->getName() ] = $field; 65 | } 66 | 67 | public function getField(string $name): Field 68 | { 69 | if (!isset($this->fields[ $name ])) { 70 | throw new \Exception(); 71 | } 72 | 73 | return $this->fields[ $name ]; 74 | } 75 | 76 | public function getFields(): array 77 | { 78 | return $this->fields; 79 | } 80 | 81 | public function getFieldsName(): array 82 | { 83 | return array_keys($this->fields); 84 | } 85 | 86 | public function getName(): string 87 | { 88 | return $this->name; 89 | } 90 | 91 | public function getIncrement(): ?int 92 | { 93 | return $this->increment; 94 | } 95 | 96 | public function hasField(string $name): bool 97 | { 98 | return isset($this->fields[ $name ]); 99 | } 100 | 101 | public function hasIncrement(): bool 102 | { 103 | return $this->increment !== null; 104 | } 105 | 106 | public function renameField(string $from, string $to): void 107 | { 108 | $this->fields[ $to ] = $this->fields[ $from ]; 109 | unset($this->fields[ $from ]); 110 | $this->fields[ $to ]->setName($to); 111 | } 112 | 113 | public function setIncrement(?int $increment): void 114 | { 115 | $this->increment = $increment; 116 | } 117 | 118 | /** 119 | * Retourne les données de la table. 120 | * 121 | * @return array 122 | * 123 | * @phpstan-return TableToArray 124 | */ 125 | public function toArray(): array 126 | { 127 | $fields = []; 128 | foreach ($this->fields as $name => $field) { 129 | $fields[ $name ] = $field->toArray(); 130 | } 131 | 132 | return [ 133 | 'fields' => $fields, 134 | 'increments' => $this->increment 135 | ]; 136 | } 137 | 138 | public function unsetField(string $name): void 139 | { 140 | unset($this->fields[ $name ]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/TableAlter.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class TableAlter extends TableBuilder 22 | { 23 | /** 24 | * Enregistre la suppression d'une colonne. 25 | * 26 | * @param string $name Nom de la colonne. 27 | * 28 | * @return void 29 | */ 30 | public function dropColumn(string $name): void 31 | { 32 | $this->table->addField(new DropType($name)); 33 | } 34 | 35 | /** 36 | * Enregistre le renommage d'une colonne. 37 | * 38 | * @param string $from Nom de la colonne. 39 | * @param string $to Nouveau nom de la colonne. 40 | * 41 | * @return void 42 | */ 43 | public function renameColumn(string $from, string $to): void 44 | { 45 | $this->table->addField(new RenameType($from, $to)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/TableBuilder.php: -------------------------------------------------------------------------------- 1 | 28 | * 29 | * @phpstan-import-type TableToArray from Table 30 | */ 31 | class TableBuilder 32 | { 33 | /** 34 | * @var Table 35 | */ 36 | protected $table; 37 | 38 | public function __construct(string $name) 39 | { 40 | $this->table = new Table($name); 41 | } 42 | 43 | /** 44 | * Enregistre un champ de type `boolean`, true ou false. 45 | * http://php.net/manual/fr/language.types.boolean.php 46 | * 47 | * @param string $name Nom du champ. 48 | * 49 | * @return Field 50 | */ 51 | public function boolean(string $name): Field 52 | { 53 | $this->table->addField(new BoolType($name)); 54 | 55 | return $this->table->getField($name); 56 | } 57 | 58 | /** 59 | * Enregistre un champ de type `char` avec une limite de taille par défaut de un caractère. 60 | * http://php.net/language.types.string 61 | * 62 | * @param string $name Nom du champ 63 | * @param int $length longueur maximum de la chaine. 64 | * 65 | * @throws TableBuilderException 66 | * 67 | * @return Field 68 | */ 69 | public function char(string $name, int $length = 1): Field 70 | { 71 | $this->table->addField(new CharType($name, $length)); 72 | 73 | return $this->table->getField($name); 74 | } 75 | 76 | /** 77 | * Enregistre un champ de type `date` sous le format Y-m-d. 78 | * 79 | * @param string $name Nom du champ. 80 | * 81 | * @return Field 82 | */ 83 | public function date(string $name): Field 84 | { 85 | $this->table->addField(new DateType($name)); 86 | 87 | return $this->table->getField($name); 88 | } 89 | 90 | /** 91 | * Enregistre un champ de type `datetime`, sous le format Y-m-d H:i:s. 92 | * 93 | * @param string $name Nom du champ. 94 | * 95 | * @return Field 96 | */ 97 | public function datetime(string $name): Field 98 | { 99 | $this->table->addField(new DateTimeType($name)); 100 | 101 | return $this->table->getField($name); 102 | } 103 | 104 | /** 105 | * Enregistre un champ de type `float`, nombre à virgule flottant. 106 | * http://php.net/manual/fr/language.types.float.php 107 | * Valeur d'insertion autorisé : 1, '1', 1.0, '1.0' (enregistrera 1.0). 108 | * 109 | * @param string $name Nom du champ. 110 | * 111 | * @return Field 112 | */ 113 | public function float(string $name): Field 114 | { 115 | $this->table->addField(new FloatType($name)); 116 | 117 | return $this->table->getField($name); 118 | } 119 | 120 | /** 121 | * Enregistre un champ de type `increments`, entier positif qui s'incrémente automatiquement. 122 | * Un seul champ increments est autorisé par table. 123 | * http://php.net/manual/fr/language.types.integer.php 124 | * 125 | * @param string $name nom du champ 126 | * 127 | * @throws TableBuilderException 128 | * 129 | * @return Field 130 | */ 131 | public function increments(string $name): Field 132 | { 133 | if ($this->table->getIncrement() !== null) { 134 | throw new TableBuilderException('Only one incremental column is allowed per table.'); 135 | } 136 | 137 | $this->table->addField(new IncrementType($name)); 138 | 139 | return $this->table->getField($name); 140 | } 141 | 142 | /** 143 | * Enregistre un champ de type `integer`, entier signié. 144 | * http://php.net/manual/fr/language.types.integer.php 145 | * Valeur d'insertion autorisé : 1, '1', 1.1, '1.1' (enregistrera 1) 146 | * 147 | * @param string $name Nom du champ. 148 | * 149 | * @return IntType 150 | */ 151 | public function integer(string $name): IntType 152 | { 153 | $this->table->addField(new IntType($name)); 154 | /** @var IntType */ 155 | return $this->table->getField($name); 156 | } 157 | 158 | /** 159 | * Enregistre un champ de type `string` avec une limite de taille par défaut de 255 caractères. 160 | * http://php.net/language.types.string 161 | * 162 | * @param string $name Nom du champ. 163 | * @param int $length Longueur maximum de la chaine. 164 | * 165 | * @throws TableBuilderException 166 | * 167 | * @return Field 168 | */ 169 | public function string(string $name, int $length = 255): Field 170 | { 171 | $this->table->addField(new StringType($name, $length)); 172 | 173 | return $this->table->getField($name); 174 | } 175 | 176 | /** 177 | * Enregistre un champ de type `text` sans limite de taille. 178 | * http://php.net/language.types.string 179 | * 180 | * @param string $name Nom du champ. 181 | * 182 | * @return Field 183 | */ 184 | public function text(string $name): Field 185 | { 186 | $this->table->addField(new TextType($name)); 187 | 188 | return $this->table->getField($name); 189 | } 190 | 191 | public function getTable(): Table 192 | { 193 | return $this->table; 194 | } 195 | 196 | /** 197 | * Créer une table à partir d'un tableau de données. 198 | * 199 | * @param string $table Nom de la table. 200 | * @param array $data Donnaées pour créer une table. 201 | * 202 | * @phpstan-param TableToArray $data 203 | * 204 | * @throws TableBuilderException 205 | * 206 | * @return Table 207 | */ 208 | public static function createTableFromArray(string $table, array $data): Table 209 | { 210 | $tableBuilder = new self($table); 211 | foreach ($data[ 'fields' ] as $name => $value) { 212 | switch ($value[ 'type' ]) { 213 | case BoolType::TYPE: 214 | case DateType::TYPE: 215 | case DateTimeType::TYPE: 216 | case FloatType::TYPE: 217 | case IncrementType::TYPE: 218 | case TextType::TYPE: 219 | $field = $tableBuilder->{$value[ 'type' ]}($name); 220 | 221 | break; 222 | case CharType::TYPE: 223 | case StringType::TYPE: 224 | $field = $tableBuilder->{$value[ 'type' ]}($name, $value[ 'length' ] ?? 0); 225 | 226 | break; 227 | case IntType::TYPE: 228 | $field = $tableBuilder->{$value[ 'type' ]}($name); 229 | 230 | if (isset($value[ 'unsigned' ])) { 231 | $field->unsigned(); 232 | } 233 | 234 | break; 235 | default: 236 | throw new TableBuilderException(sprintf('Type %s not supported.', $value[ 'type' ])); 237 | } 238 | 239 | if (isset($value[ 'nullable' ])) { 240 | $field->nullable(); 241 | } 242 | if (isset($value[ 'default' ])) { 243 | $field->valueDefault($value[ 'default' ]); 244 | } 245 | if (isset($value[ '_comment' ])) { 246 | $field->comment($value[ '_comment' ]); 247 | } 248 | } 249 | $tableBuilder->table->setIncrement($data[ 'increments' ] ?? null); 250 | 251 | return $tableBuilder->table; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/ValueToString.php: -------------------------------------------------------------------------------- 1 | 19 | * 20 | * @phpstan-import-type RowData from Schema 21 | * @phpstan-import-type Between from WhereHandler 22 | */ 23 | class Where extends WhereHandler 24 | { 25 | use ValueToString; 26 | 27 | /** 28 | * Retourne les paramètre des clauses en format pseudo SQL. 29 | * 30 | * @return string 31 | */ 32 | public function __toString(): string 33 | { 34 | $output = ''; 35 | foreach ($this->where as $where) { 36 | $output .= $where[ 'bool' ] === self::EXP_AND 37 | ? 'AND ' 38 | : 'OR '; 39 | 40 | $not = $where[ 'not' ] 41 | ? 'NOT ' 42 | : ''; 43 | $whereColumn = $where[ 'columnName' ]; 44 | switch ($where[ 'type' ]) { 45 | case 'where': 46 | $output .= sprintf('%s%s %s %s ', $not, addslashes($whereColumn), $where[ 'condition' ], self::getValueToString($where['value'])); 47 | 48 | break; 49 | case 'like': 50 | $output .= sprintf('%s %sLIKE %s ', addslashes($whereColumn), $not, self::getValueToString($where['value'])); 51 | 52 | break; 53 | case 'isNull': 54 | $output .= sprintf('%s IS %sNULL ', addslashes($whereColumn), $not); 55 | 56 | break; 57 | case 'in': 58 | $output .= sprintf( 59 | '%s %sIN %s ', 60 | addslashes($whereColumn), 61 | $not, 62 | self::getValueToString($where['value']) 63 | ); 64 | 65 | break; 66 | case 'whereGroup': 67 | $output .= sprintf('%s(%s) ', $not, self::getValueToString($where['value'])); 68 | 69 | break; 70 | case 'between': 71 | /** @var Between $value */ 72 | $value = $where['value']; 73 | $output .= sprintf( 74 | '%s %sBETWEEN %s ', 75 | addslashes($whereColumn), 76 | $not, 77 | self::getBetweenToString($value) 78 | ); 79 | 80 | break; 81 | case 'regex': 82 | $output .= sprintf('%s %sREGEX %s ', addslashes($whereColumn), $not, self::getValueToString($where['value'])); 83 | 84 | break; 85 | } 86 | } 87 | $output = trim($output, ' '); 88 | $str = strpos($output, 'AND ') === 0 89 | ? 'AND ' 90 | : 'OR '; 91 | 92 | return substr_replace($output, '', 0, strlen($str)); 93 | } 94 | 95 | /** 96 | * Retourne les nom de toutes les colonnes utilisées pour créer la clause. 97 | * 98 | * @return string[] Colonnes. 99 | */ 100 | public function getColumnNames(): array 101 | { 102 | $output = []; 103 | foreach ($this->where as $value) { 104 | if (isset($value[ 'columnNames' ])) { 105 | $output = array_merge($output, $value[ 'columnNames' ]); 106 | 107 | continue; 108 | } 109 | 110 | $output[] = self::getColumn($value[ 'columnName' ]); 111 | } 112 | 113 | return $output; 114 | } 115 | 116 | /** 117 | * Retourne TRUE si la suite de condition enregistrée valide les champs du tableau. 118 | * 119 | * @param array $row Tableau associatif de champ. 120 | * 121 | * @phpstan-param RowData $row 122 | * 123 | * @return bool 124 | */ 125 | public function execute(array $row): bool 126 | { 127 | $output = true; 128 | foreach ($this->where as $key => $value) { 129 | /* Si la clause est standard ou une sous clause. */ 130 | if ($value[ 'value' ] instanceof Where) { 131 | $predicate = $value[ 'value' ]->execute($row); 132 | } else { 133 | $predicate = self::predicate($row[ $value[ 'columnName' ] ], $value[ 'condition' ], $value[ 'value' ]); 134 | } 135 | /* Si la clause est inversé. */ 136 | if ($value[ 'not' ]) { 137 | $predicate = !$predicate; 138 | } 139 | /* Les retours des types regex et like doivent être non null. */ 140 | if ($value[ 'type' ] === 'regex' || $value[ 'type' ] === 'like') { 141 | $predicate = $predicate && $row[ $value[ 'columnName' ] ] !== null; 142 | } 143 | 144 | if ($key === 0) { 145 | $output = $predicate; 146 | 147 | continue; 148 | } 149 | $output = $value[ 'bool' ] === self::EXP_AND 150 | ? $output && $predicate 151 | : $output || $predicate; 152 | } 153 | 154 | return $output; 155 | } 156 | 157 | /** 158 | * Retourne TRUE si la suite de condition enregistrée valide les champs du tableau 159 | * par rapport à un autre tableau. 160 | * 161 | * @param array $row Tableau associatif de champ. 162 | * @param array $rowTable Tableau associatif de champ à tester. 163 | * 164 | * @phpstan-param RowData $row 165 | * @phpstan-param RowData $rowTable 166 | * 167 | * @return bool 168 | */ 169 | public function executeJoin(array $row, array &$rowTable): bool 170 | { 171 | $output = true; 172 | foreach ($this->where as $key => $value) { 173 | if ($value[ 'value' ] instanceof Where) { 174 | $predicate = $value[ 'value' ]->executeJoin($row, $rowTable); 175 | } else { 176 | /** @var array{value:string, columnName: string, condition: string, bool:string} $value */ 177 | $val = $rowTable[ self::getColumn($value[ 'value' ]) ]; 178 | 179 | $predicate = self::predicate($row[ $value[ 'columnName' ] ], $value[ 'condition' ], $val); 180 | } 181 | 182 | if ($key === 0) { 183 | $output = $predicate; 184 | 185 | continue; 186 | } 187 | $output = $value[ 'bool' ] === self::EXP_AND 188 | ? $output && $predicate 189 | : $output || $predicate; 190 | } 191 | 192 | return $output; 193 | } 194 | 195 | /** 196 | * Retourne TRUE si la condition est validée. 197 | * 198 | * @param null|scalar $column Valeur à tester. 199 | * @param string $operator Condition à réaliser. 200 | * @param array|null|scalar $value Valeur de comparaison. 201 | * 202 | * @throws \Exception 203 | * 204 | * @return bool 205 | */ 206 | protected static function predicate($column, string $operator, $value): bool 207 | { 208 | switch ($operator) { 209 | case '==': 210 | return $column == $value; 211 | case '=': 212 | case '===': 213 | return $column === $value; 214 | case '!==': 215 | return $column !== $value; 216 | case '!=': 217 | case '<>': 218 | return $column != $value; 219 | case '<': 220 | return $column < $value; 221 | case '<=': 222 | return $column <= $value; 223 | case '>': 224 | return $column > $value; 225 | case '>=': 226 | return $column >= $value; 227 | case 'in': 228 | /** @var array $value */ 229 | return in_array($column, $value); 230 | case 'regex': 231 | if ($column === null) { 232 | return false; 233 | } 234 | /** @var string $value */ 235 | return preg_match($value, (string) $column) === 1; 236 | case 'between': 237 | /** @var Between $value */ 238 | return $column >= $value[ 'min' ] && $column <= $value[ 'max' ]; 239 | } 240 | 241 | throw new OperatorNotFound( 242 | sprintf('The %s operator is not supported.', $operator) 243 | ); 244 | } 245 | 246 | /** 247 | * Retourne le nom de la colonne ou la valeur. 248 | * 249 | * @param string $value 250 | * 251 | * @return string 252 | */ 253 | protected static function getColumn(string $value): string 254 | { 255 | return strrchr($value, '.') !== false 256 | ? substr(strrchr($value, '.'), 1) 257 | : $value; 258 | } 259 | 260 | /* 261 | * @param array $between 262 | * 263 | * @phpstan-param Between $between 264 | * 265 | * @return string 266 | */ 267 | protected static function getBetweenToString(array $between): string 268 | { 269 | return sprintf( 270 | '%s AND %s', 271 | self::getValueToString($between[ 'min' ]), 272 | self::getValueToString($between[ 'max' ]) 273 | ); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/WhereHandler.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @phpstan-type Between array{min: numeric|string, max: numeric|string} 22 | * @phpstan-type WhereToArray array{ 23 | * bool: string, 24 | * columnName: string, 25 | * columnNames?: array, 26 | * condition: string, 27 | * not: bool, 28 | * type: string, 29 | * value: array|Between|null|scalar|Where, 30 | * } 31 | */ 32 | class WhereHandler 33 | { 34 | /** 35 | * Représente une expréssion AND. 36 | */ 37 | public const EXP_AND = 'and'; 38 | 39 | /** 40 | * Représente une expréssion OR. 41 | */ 42 | public const EXP_OR = 'or'; 43 | 44 | /** 45 | * Les conditions binaires autorisées. 46 | */ 47 | private const CONDITION = [ 48 | '=', '==', '===', '!==', '!=', '<>', '<', '<=', '>', '>=', 49 | 'like', 'not like', 'ilike', 'not ilike' 50 | ]; 51 | 52 | /** 53 | * Les conditions à exécuter. 54 | * 55 | * @var array 56 | * 57 | * @phpstan-var WhereToArray[] 58 | */ 59 | protected $where = []; 60 | 61 | /** 62 | * Ajoute une condition simple pour la requête. 63 | * Si la valeur du champ est égal (non égale, supérieur à, ...) par rapport à une valeur. 64 | * 65 | * @param string $columnName Nom d'une colonne. 66 | * @param string $operator Type de condition. 67 | * @param null|scalar $value Valeur de teste. 68 | * @param string $bool Porte logique de la condition (and|or). 69 | * @param bool $not Inverse la condition. 70 | * 71 | * @throws OperatorNotFound The condition is not exist. 72 | */ 73 | public function where( 74 | string $columnName, 75 | string $operator, 76 | $value, 77 | string $bool = self::EXP_AND, 78 | bool $not = false 79 | ): self { 80 | $condition = $this->filterOperator($operator); 81 | 82 | if (in_array($condition, [ 'like', 'ilike', 'not like', 'not ilike' ])) { 83 | if (!\is_string($value)) { 84 | throw new QueryException(); 85 | } 86 | $this->like( 87 | $columnName, 88 | $condition, 89 | $value, 90 | $bool, 91 | strpos($condition, 'not') !== false 92 | ); 93 | 94 | return $this; 95 | } 96 | 97 | $type = __FUNCTION__; 98 | 99 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Alias inverse de la fonction where(). 106 | * 107 | * @param null|scalar $value Valeur de teste. 108 | */ 109 | public function notWhere(string $columnName, string $operator, $value): self 110 | { 111 | $this->where($columnName, $operator, $value, self::EXP_AND, true); 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Alias avec la porte logique 'OR' de la fonction where(). 118 | * 119 | * @param null|scalar $value Valeur de teste. 120 | */ 121 | public function orWhere(string $columnName, string $operator, $value): self 122 | { 123 | $this->where($columnName, $operator, $value, self::EXP_OR); 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Alias inverse avec la porte logique 'OR' de la fonction where(). 130 | * 131 | * @param null|scalar $value Valeur de teste. 132 | */ 133 | public function orNotWhere(string $columnName, string $operator, $value): self 134 | { 135 | $this->where($columnName, $operator, $value, self::EXP_OR, true); 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Ajoute une condition between à la requête. 142 | * Si la valeur du champ est compris entre 2 valeurs. 143 | * 144 | * @param string $columnName Nom de la colonne. 145 | * @param numeric|string $min Valeur minimum ou égale. 146 | * @param numeric|string $max Valeur maximum ou égale. 147 | * @param string $bool Porte logique de la condition (and|or). 148 | * @param bool $not Inverse la condition. 149 | */ 150 | public function between( 151 | string $columnName, 152 | $min, 153 | $max, 154 | string $bool = self::EXP_AND, 155 | bool $not = false 156 | ): self { 157 | $condition = 'between'; 158 | $type = __FUNCTION__; 159 | $value = [ 'min' => $min, 'max' => $max ]; 160 | 161 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Alias inverse de la fonction between(). 168 | * 169 | * @param numeric|string $min Valeur minimum ou égale. 170 | * @param numeric|string $max Valeur maximum ou égale. 171 | */ 172 | public function notBetween(string $columnName, $min, $max): self 173 | { 174 | $this->between($columnName, $min, $max, self::EXP_AND, true); 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Alias avec la porte logique 'OR' de la fonction between(). 181 | * 182 | * @param numeric|string $min Valeur minimum ou égale. 183 | * @param numeric|string $max Valeur maximum ou égale. 184 | */ 185 | public function orBetween(string $columnName, $min, $max): self 186 | { 187 | $this->between($columnName, $min, $max, self::EXP_OR); 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * Alias inverse avec la porte logique 'OR' de la fonction between(). 194 | * 195 | * @param numeric|string $min Valeur minimum ou égale. 196 | * @param numeric|string $max Valeur maximum ou égale. 197 | */ 198 | public function orNotBetween(string $columnName, $min, $max): self 199 | { 200 | $this->between($columnName, $min, $max, self::EXP_OR, true); 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Ajoute une condition in à la requête. 207 | * Si la valeur du champs est contenu dans une liste. 208 | * 209 | * @param string $columnName Nom de la colonne. 210 | * @param array $value Valeurs à tester. 211 | * @param string $bool Porte logique de la condition (and|or). 212 | * @param bool $not Inverse la condition. 213 | */ 214 | public function in( 215 | string $columnName, 216 | array $value, 217 | string $bool = self::EXP_AND, 218 | bool $not = false 219 | ): self { 220 | $condition = 'in'; 221 | $type = __FUNCTION__; 222 | 223 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Alias inverse de la fonction in(). 230 | */ 231 | public function notIn(string $columnName, array $value): self 232 | { 233 | $this->in($columnName, $value, self::EXP_AND, true); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Alias avec la porte logique 'OR' de la fonction in(). 240 | */ 241 | public function orIn(string $columnName, array $value): self 242 | { 243 | $this->in($columnName, $value, self::EXP_OR); 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Alias inverse avec la porte logique 'OR' de la fonction in(). 250 | */ 251 | public function orNotIn(string $columnName, array $value): self 252 | { 253 | $this->in($columnName, $value, self::EXP_OR, true); 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * Ajoute une condition isNull à la requête. 260 | * Si la valeur du champ est strictement égale à null. 261 | * 262 | * @param string $columnName Nom de la colonne. 263 | * @param string $bool Porte logique de la condition (and|or). 264 | * @param bool $not Inverse la condition. 265 | */ 266 | public function isNull( 267 | string $columnName, 268 | string $bool = self::EXP_AND, 269 | bool $not = false 270 | ): self { 271 | $condition = '==='; 272 | $type = __FUNCTION__; 273 | $value = null; 274 | 275 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 276 | 277 | return $this; 278 | } 279 | 280 | /** 281 | * Alias inverse de la fonction isNull(). 282 | */ 283 | public function isNotNull(string $columnName): self 284 | { 285 | $this->isNull($columnName, self::EXP_AND, true); 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * Alias avec la porte logique 'OR' de la fonction isNull(). 292 | */ 293 | public function orIsNull(string $columnName): self 294 | { 295 | $this->isNull($columnName, self::EXP_OR); 296 | 297 | return $this; 298 | } 299 | 300 | /** 301 | * Alias inverse avec la porte logique 'OR' de la fonction isNull() 302 | */ 303 | public function orIsNotNull(string $columnName): self 304 | { 305 | $this->isNull($columnName, self::EXP_OR, true); 306 | 307 | return $this; 308 | } 309 | 310 | /** 311 | * Ajoute une condition avec une expression régulière à la requête. 312 | * 313 | * @param string $columnName Nom de la colonne. 314 | * @param string $value Expression régulière. 315 | * @param string $bool Porte logique de la condition (and|or). 316 | * @param bool $not Inverse la condition. 317 | */ 318 | public function regex( 319 | string $columnName, 320 | string $value, 321 | string $bool = self::EXP_AND, 322 | bool $not = false 323 | ): self { 324 | $condition = 'regex'; 325 | $type = __FUNCTION__; 326 | 327 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 328 | 329 | return $this; 330 | } 331 | 332 | /** 333 | * Alias inverse de la fonction regex(). 334 | */ 335 | public function notRegex(string $columnName, string $pattern): self 336 | { 337 | $this->regex($columnName, $pattern, self::EXP_AND, true); 338 | 339 | return $this; 340 | } 341 | 342 | /** 343 | * Alias avec la porte logique 'OR' de la fonction regex(). 344 | */ 345 | public function orRegex(string $columnName, string $pattern): self 346 | { 347 | $this->regex($columnName, $pattern, self::EXP_OR); 348 | 349 | return $this; 350 | } 351 | 352 | /** 353 | * Alias inverse avec la porte logique 'OR' de la fonction regex() 354 | */ 355 | public function orNotRegex(string $columnName, string $pattern): self 356 | { 357 | $this->regex($columnName, $pattern, self::EXP_OR, true); 358 | 359 | return $this; 360 | } 361 | 362 | /** 363 | * Ajoute une sous-condition pour la requête. 364 | */ 365 | public function whereGroup( 366 | \Closure $callable, 367 | string $bool = self::EXP_AND, 368 | bool $not = false 369 | ): void { 370 | $where = new Where(); 371 | call_user_func_array($callable, [ &$where ]); 372 | 373 | $this->where[] = [ 374 | 'type' => __FUNCTION__, 375 | 'columnName' => '', 376 | 'columnNames' => $where->getColumnNames(), 377 | 'condition' => '', 378 | 'value' => $where, 379 | 'bool' => $bool, 380 | 'not' => $not 381 | ]; 382 | } 383 | 384 | /** 385 | * Alias inverse de la fonction whereGroup(). 386 | */ 387 | public function notWhereGroup(\Closure $callable): self 388 | { 389 | $this->whereGroup($callable, self::EXP_AND, true); 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * Alias avec la porte logique 'OR' de la fonction whereGroup(). 396 | */ 397 | public function orWhereGroup(\Closure $callable): self 398 | { 399 | $this->whereGroup($callable, self::EXP_OR); 400 | 401 | return $this; 402 | } 403 | 404 | /** 405 | * Alias inverse avec la porte logique 'OR' de la fonction whereGroup(). 406 | */ 407 | public function orNotWhereGroup(\Closure $callable): self 408 | { 409 | $this->whereGroup($callable, self::EXP_OR, true); 410 | 411 | return $this; 412 | } 413 | 414 | /** 415 | * Ajoute une condition like pour la requête. 416 | * 417 | * @param string $columnName 418 | * @param string $operator 419 | * @param string $pattern 420 | * @param string $bool 421 | * @param bool $not 422 | * 423 | * @return void 424 | */ 425 | protected function like( 426 | string $columnName, 427 | string $operator, 428 | string $pattern, 429 | string $bool = self::EXP_AND, 430 | bool $not = false 431 | ): void { 432 | /* Protection des caractères spéciaux des expressions rationnelles autre que celle imposée. */ 433 | $str = preg_quote($pattern, '/'); 434 | 435 | /* Le paterne commun au 4 conditions. */ 436 | $value = '/^' . strtr($str, [ '%' => '.*' ]); 437 | 438 | /* Pour rendre la regex inssencible à la case. */ 439 | $value .= $operator === 'like' || $operator === 'not like' 440 | ? '$/' 441 | : '$/i'; 442 | 443 | $type = __FUNCTION__; 444 | $condition = 'regex'; 445 | 446 | $this->where[] = ['bool' => $bool, 'columnName' => $columnName, 'condition' => $condition, 'not' => $not, 'type' => $type, 'value' => $value]; 447 | } 448 | 449 | /** 450 | * Filtre l'opérateur. 451 | * 452 | * @param string $operator 453 | * 454 | * @throws OperatorNotFound 455 | * 456 | * @return string 457 | */ 458 | private function filterOperator(string $operator): string 459 | { 460 | $condition = strtolower($operator); 461 | 462 | if (!in_array($condition, self::CONDITION)) { 463 | throw new OperatorNotFound( 464 | sprintf('The condition %s is not exist.', $operator) 465 | ); 466 | } 467 | 468 | return $condition; 469 | } 470 | } 471 | --------------------------------------------------------------------------------