├── LICENSE ├── README.md ├── composer.json └── src ├── ArrayExpression.php ├── ArrayExpressionBuilder.php ├── ColumnSchema.php ├── CompositeExpression.php ├── CompositeExpressionBuilder.php ├── CompositeParser.php ├── QueryBuilder.php ├── Schema.php └── validators ├── AbstractCompositeValidator.php ├── CompositeFilterEmpty.php └── CompositeValidator.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tigrov 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 | yii2-pgsql 2 | ============== 3 | 4 | Improved PostgreSQL schemas for Yii2. 5 | 6 | Yii 2.0.14 and above supports `array` and `json` DB types. 7 | 8 | Supports follow types for ActiveRecord models: 9 | * `array`, Yii 2.0.14 and above supports `array` DB type 10 | * `json`, Yii 2.0.14 and above supports `json` DB type 11 | * [`composite`](docs/composite.md), https://www.postgresql.org/docs/current/static/rowtypes.html 12 | * `domain`, https://www.postgresql.org/docs/current/static/sql-createdomain.html 13 | * fixes type `bit`, issue [#7682](https://github.com/yiisoft/yii2/issues/7682) 14 | * converts Postgres types `timestamp`, `date` and `time` to PHP type `\DateTime` and vice versa. 15 | 16 | [![Latest Stable Version](https://poser.pugx.org/Tigrov/yii2-pgsql/v/stable)](https://packagist.org/packages/Tigrov/yii2-pgsql) 17 | [![Build Status](https://travis-ci.org/Tigrov/yii2-pgsql.svg?branch=master)](https://travis-ci.org/Tigrov/yii2-pgsql) 18 | 19 | Limitation 20 | ------------ 21 | 22 | Since version 1.2.0 requires Yii 2.0.14 and above. 23 | You can use version 1.1.11 if you have Yii 2.0.13 and below. 24 | 25 | Installation 26 | ------------ 27 | 28 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 29 | 30 | Either run 31 | 32 | ``` 33 | php composer.phar require --prefer-dist tigrov/yii2-pgsql 34 | ``` 35 | 36 | or add 37 | 38 | ``` 39 | "tigrov/yii2-pgsql": "~1.0" 40 | ``` 41 | 42 | to the require section of your `composer.json` file. 43 | 44 | 45 | Configuration 46 | ------------- 47 | Once the extension is installed, add following code to your application configuration: 48 | 49 | ```php 50 | return [ 51 | //... 52 | 'components' => [ 53 | 'db' => [ 54 | 'class' => 'yii\db\Connection', 55 | 'dsn' => 'pgsql:host=localhost;dbname=', 56 | 'username' => 'postgres', 57 | 'password' => '', 58 | 'schemaMap' => [ 59 | 'pgsql'=> 'tigrov\pgsql\Schema', 60 | ], 61 | ], 62 | ], 63 | ]; 64 | ``` 65 | 66 | Specify the desired types for a table 67 | ```sql 68 | CREATE TABLE public.model ( 69 | id serial NOT NULL, 70 | attribute1 text[], 71 | attribute2 jsonb, 72 | attribute3 timestamp DEFAULT now(), 73 | CONSTRAINT model_pkey PRIMARY KEY (id) 74 | ); 75 | ``` 76 | 77 | Configure Model's rules 78 | ```php 79 | /** 80 | * @property string[] $attribute1 array of string 81 | * @property array $attribute2 associative array or just array 82 | * @property integer|string|\DateTime $attribute3 for more information about the type see \Yii::$app->formatter->asDatetime() 83 | */ 84 | class Model extends ActiveRecord 85 | { 86 | //... 87 | public function rules() 88 | { 89 | return [ 90 | [['attribute1'], 'each', 'rule' => ['string']], 91 | [['attribute2', 'attribute3'], 'safe'], 92 | ]; 93 | } 94 | } 95 | ``` 96 | 97 | Usage 98 | ----- 99 | 100 | You can then save array, json and timestamp types in database as follows: 101 | 102 | ```php 103 | /** 104 | * @var ActiveRecord $model 105 | */ 106 | $model->attribute1 = ['some', 'values', 'of', 'array']; 107 | $model->attribute2 = ['some' => 'values', 'of' => 'array']; 108 | $model->attribute3 = new \DateTime('now'); 109 | $model->save(); 110 | ``` 111 | 112 | and then use them in your code 113 | ```php 114 | /** 115 | * @var ActiveRecord $model 116 | */ 117 | $model = Model::findOne($pk); 118 | $model->attribute1; // is array 119 | $model->attribute2; // is associative array (decoded json) 120 | $model->attribute3; // is \DateTime 121 | ``` 122 | 123 | [Composite types](docs/composite.md) 124 | 125 | License 126 | ------- 127 | 128 | [MIT](LICENSE) 129 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tigrov/yii2-pgsql", 3 | "description": "Improved PostgreSQL schemas for Yii2", 4 | "keywords": ["yii2", "extension", "pgsql", "postgres", "postgresql", "array", "json", "bit", "datetime", "timestamp", "composite", "domain"], 5 | "type": "yii2-extension", 6 | "license": "MIT", 7 | "support": { 8 | "source": "https://github.com/tigrov/yii2-pgsql" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Sergei Tigrov", 13 | "email": "rrr-r@ya.ru" 14 | } 15 | ], 16 | "require": { 17 | "yiisoft/yii2": "^2.0.29" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "4.8.34" 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "composer", 25 | "url": "https://asset-packagist.org" 26 | } 27 | ], 28 | "autoload": { 29 | "psr-4": { 30 | "tigrov\\pgsql\\": "src/" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/ArrayExpression.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | /** 10 | * ArrayExpression is the improved class which represents an array SQL expression. 11 | * 12 | * @author Sergei Tigrov 13 | */ 14 | class ArrayExpression extends \yii\db\ArrayExpression 15 | { 16 | /** 17 | * @var ColumnSchema the metadata of a column in a PostgreSQL database table. 18 | */ 19 | private $column; 20 | 21 | /** 22 | * {@inheritdoc} 23 | * @param ColumnSchema|null $column the metadata of a column in a PostgreSQL database table. 24 | */ 25 | public function __construct($value, $type = null, $dimension = 1, $column = null) 26 | { 27 | parent::__construct($value, $type, $dimension); 28 | $this->column = $column; 29 | } 30 | 31 | /** 32 | * @return null|ColumnSchema 33 | * @see column 34 | */ 35 | public function getColumn() 36 | { 37 | return $this->column; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getType() 44 | { 45 | return parent::getType() ?: ($this->column ? $this->column->dbType : null); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ArrayExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use yii\db\ExpressionInterface; 10 | 11 | /** 12 | * ArrayExpressionBuilder is the improved class which builds [[ArrayExpression]] for PostgreSQL DBMS. 13 | * 14 | * @author Sergei Tigrov 15 | */ 16 | class ArrayExpressionBuilder extends \yii\db\pgsql\ArrayExpressionBuilder 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected function getTypehint(\yii\db\ArrayExpression $expression) 22 | { 23 | $type = $expression->getType(); 24 | if ($type === null) { 25 | return ''; 26 | } 27 | 28 | if ($expression instanceof ArrayExpression) { 29 | $column = $expression->getColumn(); 30 | if ($column !== null) { 31 | if ($column->type === Schema::TYPE_COMPOSITE || $column->enumValues !== null) { 32 | if (strpos($type, '.') === false) { 33 | $schema = $this->queryBuilder->db->schema->defaultSchema; 34 | $type = $schema . '.' . $type; 35 | } 36 | } 37 | } 38 | } 39 | 40 | $result = '::' . $type; 41 | $result .= str_repeat('[]', $expression->getDimension()); 42 | 43 | return $result; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | * @return mixed 49 | */ 50 | protected function typecastValue(\yii\db\ArrayExpression $expression, $value) 51 | { 52 | if ($expression instanceof ArrayExpression) { 53 | $column = $expression->getColumn(); 54 | if ($column !== null) { 55 | if ($value instanceof ExpressionInterface) { 56 | return $value; 57 | } 58 | 59 | return $column->dbTypecastValue($value); 60 | } 61 | } 62 | 63 | return parent::typecastValue($expression, $value); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ColumnSchema.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use yii\db\ExpressionInterface; 10 | use yii\db\JsonExpression; 11 | use yii\db\PdoValue; 12 | 13 | /** 14 | * ColumnSchema is the improved class which describes the metadata of a column in a PostgreSQL database table 15 | * 16 | * @author Sergei Tigrov 17 | */ 18 | class ColumnSchema extends \yii\db\pgsql\ColumnSchema 19 | { 20 | /** 21 | * @var string the delimiter character to be used between values in arrays made of this type. 22 | */ 23 | public $delimiter; 24 | 25 | /** 26 | * @var ColumnSchema[]|null columns of composite type 27 | */ 28 | public $columns; 29 | 30 | /** 31 | * @inheritdoc 32 | */ 33 | public function dbTypecast($value) 34 | { 35 | if ($value instanceof ExpressionInterface) { 36 | return $value; 37 | } 38 | 39 | if ($this->dimension > 0) { 40 | if ($value === null) { 41 | return null; 42 | } 43 | 44 | return new ArrayExpression($value, $this->dbType, $this->dimension, $this); 45 | } 46 | 47 | return $this->dbTypecastValue($value); 48 | } 49 | 50 | /** 51 | * Converts the input value according to [[type]] and [[dbType]] for use in a db query. 52 | * @param mixed $value input value 53 | * @return mixed converted value. 54 | */ 55 | public function dbTypecastValue($value) 56 | { 57 | if ($value === null) { 58 | return null; 59 | } 60 | 61 | switch ($this->type) { 62 | case Schema::TYPE_BIT: 63 | return decbin($value); 64 | case Schema::TYPE_BINARY: 65 | return is_string($value) ? new PdoValue($value, \PDO::PARAM_LOB) : $value; 66 | case Schema::TYPE_TIMESTAMP: 67 | case Schema::TYPE_DATETIME: 68 | return \Yii::$app->formatter->asDatetime($value, 'yyyy-MM-dd HH:mm:ss'); 69 | case Schema::TYPE_DATE: 70 | return \Yii::$app->formatter->asDate($value, 'yyyy-MM-dd'); 71 | case Schema::TYPE_TIME: 72 | return \Yii::$app->formatter->asTime($value, 'HH:mm:ss'); 73 | case Schema::TYPE_JSON: 74 | return new JsonExpression($value, $this->dbType); 75 | case Schema::TYPE_COMPOSITE: 76 | return $this->createCompositeExpression($value); 77 | } 78 | 79 | return $this->typecast($value); 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | */ 85 | public function phpTypecast($value) 86 | { 87 | if ($this->dimension > 0) { 88 | if (!is_array($value)) { 89 | $value = $this->getArrayParser()->parse($value); 90 | } 91 | if (is_array($value)) { 92 | array_walk_recursive($value, function (&$val, $key) { 93 | $val = $this->phpTypecastValue($val); 94 | }); 95 | } 96 | 97 | return $value; 98 | } 99 | 100 | return $this->phpTypecastValue($value); 101 | } 102 | 103 | /** 104 | * Converts the input value according to [[phpType]] after retrieval from the database. 105 | * @param mixed $value input value 106 | * @return mixed converted value 107 | */ 108 | protected function phpTypecastValue($value) 109 | { 110 | if ($value === null) { 111 | return null; 112 | } 113 | 114 | switch ($this->type) { 115 | case Schema::TYPE_BOOLEAN: 116 | switch (strtolower($value)) { 117 | case 't': 118 | case 'true': 119 | return true; 120 | case 'f': 121 | case 'false': 122 | return false; 123 | } 124 | return (bool) $value; 125 | case Schema::TYPE_BIT: 126 | return bindec($value); 127 | case Schema::TYPE_BINARY: 128 | return is_string($value) && strncmp($value, '\\x', 2) === 0 ? pack('H*', substr($value, 2)) : $value; 129 | case Schema::TYPE_JSON: 130 | return json_decode($value, true); 131 | case Schema::TYPE_TIMESTAMP: 132 | case Schema::TYPE_TIME: 133 | case Schema::TYPE_DATE: 134 | case Schema::TYPE_DATETIME: 135 | return new \DateTime($value); 136 | case Schema::TYPE_COMPOSITE: 137 | return $this->phpTypecastComposite($value); 138 | } 139 | 140 | return $this->typecast($value); 141 | } 142 | 143 | /** 144 | * Converts the composite type to PHP 145 | * @param array|string|object|null $value the value to be converted 146 | * @return array|object|null Composite object as described in `ColumnSchema::$phpType` (@see `Schema::$compositeMap`) or `null` 147 | */ 148 | public function phpTypecastComposite($value) 149 | { 150 | if (is_string($value)) { 151 | $value = $this->getCompositeParser()->parse($value); 152 | } 153 | if (is_array($value)) { 154 | $result = []; 155 | $fields = array_keys($this->columns); 156 | foreach ($value as $i => $item) { 157 | $field = is_int($i) ? $fields[$i] : $i; 158 | if (isset($this->columns[$field])) { 159 | $result[$field] = $this->columns[$field]->phpTypecast($item); 160 | } 161 | } 162 | 163 | return $this->createCompositeObject($result); 164 | } elseif (!$value instanceof $this->phpType) { 165 | return null; 166 | } 167 | 168 | return $value; 169 | } 170 | 171 | /** 172 | * Creates an object for the composite type. 173 | * @param array $values to be passed to the class constructor 174 | * @return mixed 175 | */ 176 | public function createCompositeObject($values) 177 | { 178 | switch ($this->phpType) { 179 | case 'array': 180 | return $values; 181 | case 'object': 182 | return (object)$values; 183 | } 184 | 185 | return \Yii::createObject($this->phpType, [$values]); 186 | } 187 | 188 | /** 189 | * Creates CompositeExpression object 190 | * 191 | * @param array|mixed $value the composite type content. Either represented as an array of values or a composite 192 | * object which corresponds to Schema::compositeMap and could be converted to array. 193 | * @return CompositeExpression 194 | */ 195 | public function createCompositeExpression($value) 196 | { 197 | return new CompositeExpression($value, $this->dbType, $this); 198 | } 199 | 200 | /** 201 | * Creates instance of CompositeParser 202 | * 203 | * @return CompositeParser 204 | */ 205 | protected function getCompositeParser() 206 | { 207 | static $parser = null; 208 | 209 | if ($parser === null) { 210 | $parser = new CompositeParser(); 211 | } 212 | 213 | return $parser; 214 | } 215 | } -------------------------------------------------------------------------------- /src/CompositeExpression.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use yii\db\ExpressionInterface; 10 | 11 | /** 12 | * Class CompositeExpression represents data that should be encoded to composite type SQL expression. 13 | * 14 | * For example: 15 | * 16 | * ```php 17 | * new CompositeExpression(['a' => 1, 'b' => 2]); // will be encoded to 'ROW(1,2)' 18 | * ``` 19 | * 20 | * @author Sergei Tigrov 21 | */ 22 | class CompositeExpression implements ExpressionInterface 23 | { 24 | /** 25 | * @var array|mixed the composite type content. Either represented as an array of values or a composite object 26 | * which corresponds to Schema::compositeMap and could be converted to array. 27 | */ 28 | protected $value; 29 | /** 30 | * @var null|string the type of the composite type. Defaults to `null` which means the type is 31 | * not explicitly specified. 32 | * 33 | * Note that in case when type is not specified explicitly and DBMS can not guess it from the context, 34 | * SQL error will be raised. 35 | */ 36 | private $type; 37 | /** 38 | * @var ColumnSchema describes the metadata of a column in a PostgreSQL database table 39 | */ 40 | private $column; 41 | 42 | 43 | /** 44 | * CompositeExpression constructor. 45 | * 46 | * @param array|mixed $value the composite type content. Either represented as an array of values or a composite 47 | * object which corresponds to Schema::compositeMap and could be converted to array. 48 | * @param ColumnSchema|null $column the metadata of a column in a PostgreSQL database table. 49 | */ 50 | public function __construct($value, $type = null, $column = null) 51 | { 52 | $this->value = $value; 53 | $this->type = $type; 54 | $this->column = $column; 55 | } 56 | 57 | /** 58 | * @return mixed 59 | * @see value 60 | */ 61 | public function getValue() 62 | { 63 | return $this->value; 64 | } 65 | 66 | /** 67 | * @return null|string 68 | * @see type 69 | */ 70 | public function getType() 71 | { 72 | return $this->type ?: ($this->column ? $this->column->dbType : null); 73 | } 74 | 75 | /** 76 | * @return ColumnSchema|null 77 | * @see column 78 | */ 79 | public function getColumn() 80 | { 81 | return $this->column; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/CompositeExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use yii\base\Arrayable; 10 | use yii\db\ExpressionBuilderInterface; 11 | use yii\db\ExpressionBuilderTrait; 12 | use yii\db\ExpressionInterface; 13 | 14 | /** 15 | * Class CompositeExpressionBuilder builds [[CompositeExpression]] for PostgreSQL DBMS. 16 | * 17 | * @author Sergei Tigrov 18 | */ 19 | class CompositeExpressionBuilder implements ExpressionBuilderInterface 20 | { 21 | use ExpressionBuilderTrait; 22 | 23 | /** 24 | * {@inheritdoc} 25 | * @param ExpressionInterface|CompositeExpression $expression the expression to be built 26 | */ 27 | public function build(ExpressionInterface $expression, array &$params = []) 28 | { 29 | $value = $expression->getValue(); 30 | if ($value === null) { 31 | return 'NULL'; 32 | } 33 | 34 | $placeholders = $this->buildPlaceholders($expression, $params); 35 | if (empty($placeholders)) { 36 | return "'()'"; 37 | } 38 | 39 | return 'ROW(' . implode(', ', $placeholders) . ')' . $this->getTypehint($expression); 40 | } 41 | 42 | /** 43 | * Builds placeholders array out of $expression values 44 | * @param ExpressionInterface|CompositeExpression $expression 45 | * @param array $params the binding parameters. 46 | * @return array 47 | */ 48 | protected function buildPlaceholders(ExpressionInterface $expression, &$params) 49 | { 50 | $value = $this->prepareValue($expression); 51 | 52 | $columns = $expression->getColumn()->columns; 53 | $fields = array_keys($columns); 54 | 55 | $placeholders = []; 56 | foreach ($value as $i => $item) { 57 | $field = is_int($i) ? $fields[$i] : $i; 58 | if (isset($columns[$field])) { 59 | $item = $columns[$field]->dbTypecast($item); 60 | if ($item instanceof ExpressionInterface) { 61 | $placeholders[] = $this->queryBuilder->buildExpression($item, $params); 62 | continue; 63 | } 64 | 65 | $placeholders[] = $this->queryBuilder->bindParam($item, $params); 66 | } 67 | } 68 | 69 | return $placeholders; 70 | } 71 | 72 | /** 73 | * @param CompositeExpression $expression 74 | * @return string the typecast expression based on [[type]]. 75 | */ 76 | protected function getTypehint(CompositeExpression $expression) 77 | { 78 | $type = $expression->getType(); 79 | if ($type === null) { 80 | return ''; 81 | } 82 | 83 | if (strpos($type, '.') === false) { 84 | $schema = $this->queryBuilder->db->schema->defaultSchema; 85 | $type = $schema . '.' . $type; 86 | } 87 | 88 | return '::' . $type; 89 | } 90 | 91 | /** 92 | * Sort a composite value in the order of the columns and append skipped values as default value 93 | * e.g. if default is (0,USD) and $value is ['value' => 10] or [10] 94 | * then will be converted as ['value' => 10, 'currency_code' => 'USD'] 95 | * @param CompositeExpression $expression 96 | * @return array 97 | */ 98 | protected function prepareValue(CompositeExpression $expression) 99 | { 100 | $value = $expression->getValue(); 101 | $value = $this->toArray($value); 102 | $column = $expression->getColumn(); 103 | if ($column) { 104 | $fields = array_keys($column->columns); 105 | $keys = array_keys($value); 106 | 107 | if ($fields !== $keys) { 108 | $defaultValue = $column->defaultValue !== null ? $this->toArray($column->defaultValue) : []; 109 | if (count(array_filter($keys, 'is_string'))) { 110 | $list = []; 111 | foreach ($fields as $field) { 112 | $list[$field] = array_key_exists($field, $value) ? $value[$field] : (isset($defaultValue[$field]) ? $defaultValue[$field] : null); 113 | } 114 | 115 | return $list; 116 | } elseif (count($keys) < count($fields)) { 117 | $skippedKeys = array_slice($fields, count($keys)); 118 | foreach ($skippedKeys as $key) { 119 | array_push($value, (isset($defaultValue[$key]) ? $defaultValue[$key] : null)); 120 | } 121 | } 122 | } 123 | } 124 | 125 | return $value; 126 | } 127 | 128 | /** 129 | * Converts object to array 130 | * @param array|object $value the value to be converted 131 | * @return array 132 | */ 133 | protected function toArray($value) 134 | { 135 | return $value instanceof Arrayable 136 | ? $value->toArray() 137 | : (array) $value; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/CompositeParser.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | /** 10 | * The class converts PostgreSQL composite type representation to PHP array 11 | * 12 | * @author Sergei Tigrov 13 | */ 14 | class CompositeParser 15 | { 16 | /** 17 | * Converts PostgreSQL composite type representation to PHP array 18 | * 19 | * @param string $value string to be converted 20 | * @return array|null 21 | */ 22 | public function parse($value) 23 | { 24 | if ($value === null) { 25 | return null; 26 | } 27 | 28 | if ($value == '()') { 29 | return [null]; 30 | } 31 | 32 | return $this->parseComposite($value); 33 | } 34 | 35 | /** 36 | * Parses PostgreSQL composite type encoded in string 37 | * 38 | * @param string $value 39 | * @param int $i parse starting position 40 | * @return array 41 | */ 42 | private function parseComposite($value, &$i = 0) 43 | { 44 | $result = []; 45 | $length = strlen($value); 46 | for(++$i; $i < $length; ++$i) { 47 | switch ($value[$i]) { 48 | case ')': 49 | break 2; 50 | case ',': 51 | if (empty($result)) { 52 | $result[] = null; 53 | } 54 | if (in_array($value[$i + 1], [',', ')'], true)) { 55 | $result[] = null; 56 | } 57 | break; 58 | default: 59 | $result[] = $this->parseString($value, $i); 60 | } 61 | } 62 | 63 | return $result; 64 | } 65 | 66 | /** 67 | * Parses PostgreSQL encoded string 68 | * 69 | * @param string $value 70 | * @param int $i parse starting position 71 | * @return string 72 | */ 73 | private function parseString($value, &$i) 74 | { 75 | $isQuoted = $value[$i] === '"'; 76 | $endChars = $isQuoted ? ['"'] : [',', ')']; 77 | $result = ''; 78 | $length = strlen($value); 79 | for ($i += $isQuoted ? 1 : 0; $i < $length; ++$i) { 80 | if (in_array($value[$i], ['\\', '"'], true) && in_array($value[$i + 1], [$value[$i], '"'], true)) { 81 | ++$i; 82 | } elseif (in_array($value[$i], $endChars, true)) { 83 | break; 84 | } 85 | 86 | $result .= $value[$i]; 87 | } 88 | 89 | $i -= $isQuoted ? 0 : 1; 90 | 91 | return $result; 92 | } 93 | } -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use yii\db\Expression; 10 | 11 | /** 12 | * QueryBuilder is the improved query builder for PostgreSQL databases. 13 | * 14 | * @author Sergei Tigrov 15 | */ 16 | class QueryBuilder extends \yii\db\pgsql\QueryBuilder 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function init() 22 | { 23 | $this->typeMap[Schema::TYPE_BIT] = 'bit'; 24 | 25 | parent::init(); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function defaultExpressionBuilders() 32 | { 33 | return array_merge(parent::defaultExpressionBuilders(), [ 34 | 'tigrov\pgsql\ArrayExpression' => 'tigrov\pgsql\ArrayExpressionBuilder', 35 | 'tigrov\pgsql\CompositeExpression' => 'tigrov\pgsql\CompositeExpressionBuilder', 36 | ]); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Schema.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | namespace tigrov\pgsql; 8 | 9 | use Yii; 10 | use yii\db\TableSchema; 11 | 12 | /** 13 | * Schema is the improved class for retrieving metadata from a PostgreSQL database 14 | * (version 9.x and above). 15 | * 16 | * @author Sergei Tigrov 17 | */ 18 | class Schema extends \yii\db\pgsql\Schema 19 | { 20 | const TYPE_BIT = 'bit'; 21 | const TYPE_COMPOSITE = 'composite'; 22 | 23 | const DATE_TYPES = [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME]; 24 | const CURRENT_TIME_DEFAULTS = ['now()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME']; 25 | 26 | /** 27 | * @var array mapping from composite column types (keys) to PHP types (classes in configuration style). 28 | * `array` by default, `object` also available as PHP type then a result will be converted to \stdClass. 29 | * The result will be passed to the class constructor as an array. 30 | * Example of the class constructor: 31 | * ```php 32 | * public function __construct($config = []) 33 | * { 34 | * if (!empty($config)) { 35 | * \Yii::configure($this, $config); 36 | * } 37 | * } 38 | * ``` 39 | */ 40 | public $compositeMap = []; 41 | 42 | public function init() 43 | { 44 | $this->typeMap['bit'] = static::TYPE_BIT; 45 | $this->typeMap['bit varying'] = static::TYPE_BIT; 46 | $this->typeMap['varbit'] = static::TYPE_BIT; 47 | 48 | parent::init(); 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | * 54 | * @return QueryBuilder query builder instance 55 | */ 56 | public function createQueryBuilder() 57 | { 58 | return new QueryBuilder($this->db); 59 | } 60 | 61 | /** 62 | * @inheritdoc 63 | */ 64 | protected function findColumns($table) 65 | { 66 | $tableName = $this->db->quoteValue($table->name); 67 | $schemaName = $this->db->quoteValue($table->schemaName); 68 | 69 | $orIdentity = ''; 70 | if (version_compare($this->db->serverVersion, '12.0', '>=')) { 71 | $orIdentity = 'OR a.attidentity != \'\''; 72 | } 73 | 74 | $sql = << 0 THEN tb.typdelim ELSE t.typdelim END AS delimiter, 97 | COALESCE(td.oid, tb.oid, a.atttypid) AS type_id, 98 | t.typname AS attr_type 99 | FROM 100 | pg_class c 101 | LEFT JOIN pg_attribute a ON a.attrelid = c.oid 102 | LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum 103 | LEFT JOIN pg_type t ON a.atttypid = t.oid 104 | LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid OR t.typbasetype > 0 AND t.typbasetype = tb.oid 105 | LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid 106 | LEFT JOIN pg_namespace d ON d.oid = c.relnamespace 107 | LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p' 108 | WHERE 109 | a.attnum > 0 AND t.typname != '' AND NOT a.attisdropped 110 | AND c.relname = {$tableName} 111 | AND d.nspname = {$schemaName} 112 | ORDER BY 113 | a.attnum; 114 | SQL; 115 | 116 | $columns = $this->db->createCommand($sql)->queryAll(); 117 | if (empty($columns)) { 118 | return false; 119 | } 120 | foreach ($columns as $column) { 121 | if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) { 122 | $column = array_change_key_case($column, CASE_LOWER); 123 | } 124 | $column = $this->loadColumnSchema($column); 125 | $table->columns[$column->name] = $column; 126 | if ($column->isPrimaryKey) { 127 | $table->primaryKey[] = $column->name; 128 | if ($table->sequenceName === null) { 129 | $table->sequenceName = $column->sequenceName; 130 | } 131 | $column->defaultValue = null; 132 | } elseif ($column->defaultValue) { 133 | if (in_array($column->type, static::DATE_TYPES) && in_array($column->defaultValue, static::CURRENT_TIME_DEFAULTS)) { 134 | $column->defaultValue = new \DateTime(); 135 | } elseif (preg_match("/^B?'(.*?)'::/", $column->defaultValue, $matches)) { 136 | $column->defaultValue = $column->phpTypecast($matches[1]); 137 | } elseif (preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->defaultValue, $matches)) { 138 | if ($matches[2] === 'NULL') { 139 | $column->defaultValue = null; 140 | } else { 141 | $column->defaultValue = $column->phpTypecast($matches[2]); 142 | } 143 | } else { 144 | $column->defaultValue = $column->phpTypecast($column->defaultValue); 145 | } 146 | } 147 | } 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * @inheritdoc 154 | * 155 | * @return ColumnSchema the column schema object 156 | */ 157 | protected function loadColumnSchema($info) 158 | { 159 | list($info['numeric_precision'], $info['numeric_scale']) = $this->getPrecisionScale($info); 160 | 161 | $column = parent::loadColumnSchema($info); 162 | $column->dbType = ltrim($info['attr_type'], '_'); 163 | if ($column->size === null && $info['modifier'] != -1 && !$column->scale) { 164 | $column->size = (int) $info['modifier'] - 4; 165 | } 166 | $column->delimiter = $info['delimiter']; 167 | 168 | // b for a base type, c for a composite type, e for an enum type, p for a pseudo-type. 169 | if ($info['type_type'] == 'c') { 170 | $column->type = self::TYPE_COMPOSITE; 171 | $column->phpType = 'array'; 172 | 173 | $composite = new TableSchema(); 174 | $this->resolveTableNames($composite, $info['data_type']); 175 | if ($this->findColumns($composite)) { 176 | $column->columns = $composite->columns; 177 | } 178 | 179 | if (isset($this->compositeMap[$composite->schemaName . '.' . $composite->name])) { 180 | $column->phpType = $this->compositeMap[$composite->schemaName . '.' . $composite->name]; 181 | } elseif (isset($this->compositeMap[$composite->name])) { 182 | $column->phpType = $this->compositeMap[$composite->name]; 183 | } 184 | } 185 | 186 | return $column; 187 | } 188 | 189 | /** 190 | * @return ColumnSchema 191 | * @throws \yii\base\InvalidConfigException 192 | */ 193 | protected function createColumnSchema() 194 | { 195 | return Yii::createObject(ColumnSchema::className()); 196 | } 197 | 198 | /** 199 | * @inheritdoc 200 | */ 201 | protected function getColumnPhpType($column) 202 | { 203 | static $typeMap = [ 204 | // abstract type => php type 205 | self::TYPE_BIT => 'integer', 206 | ]; 207 | 208 | if (isset($typeMap[$column->type])) { 209 | return $typeMap[$column->type]; 210 | } 211 | 212 | return parent::getColumnPhpType($column); 213 | } 214 | 215 | protected function getPrecisionScale($info) 216 | { 217 | switch ($info['type_id']) { 218 | case 21: /*int2*/ 219 | return [16, 0]; 220 | case 23: /*int4*/ 221 | return [32, 0]; 222 | case 20: /*int8*/ 223 | return [64, 0]; 224 | case 700: /*float4*/ 225 | return [24, 0]; /*FLT_MANT_DIG*/ 226 | case 701: /*float8*/ 227 | return [53, null]; /*DBL_MANT_DIG*/ 228 | case 1700: /*numeric*/ 229 | return $info['modifier'] = -1 230 | ? [null, null] 231 | : [(($info['modifier'] - 4) >> 16) & 65535, ($info['modifier'] - 4) & 65535]; 232 | } 233 | 234 | return [null, null]; 235 | } 236 | } -------------------------------------------------------------------------------- /src/validators/AbstractCompositeValidator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class AbstractCompositeValidator extends Validator 15 | { 16 | /** @var object Class of the composite value */ 17 | public $compositeClass; 18 | 19 | /** 20 | * @param Model $model 21 | * @param string $attribute 22 | * @param mixed $value 23 | * @return object 24 | * @throws \yii\base\InvalidConfigException 25 | */ 26 | public function castValue($model, $attribute, $value) 27 | { 28 | if (!$value instanceof Model) { 29 | if ($model instanceof ActiveRecord) { 30 | /** @var \tigrov\pgsql\ColumnSchema $compositeColumn */ 31 | $compositeColumn = $model::getTableSchema()->getColumn($attribute); 32 | $value = $compositeColumn->phpTypecastComposite($value ?: $compositeColumn->defaultValue ?: []); 33 | } elseif ($this->compositeClass) { 34 | $value = \Yii::createObject($this->compositeClass, [is_array($value) ? $value : []]); 35 | } 36 | } 37 | 38 | return $value; 39 | } 40 | } -------------------------------------------------------------------------------- /src/validators/CompositeFilterEmpty.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class CompositeFilterEmpty extends AbstractCompositeValidator 16 | { 17 | public $isArray = false; 18 | 19 | /** @var object Class of the composite value */ 20 | public $compositeClass; 21 | 22 | /** 23 | * @inheritdoc 24 | * @param ActiveRecord $model the model being validated 25 | */ 26 | public function validateAttribute($model, $attribute) 27 | { 28 | $value = $model->$attribute; 29 | if ($this->isArray) { 30 | $list = []; 31 | foreach ($value as $v) { 32 | if (!$this->checkRequired($model, $attribute, $v)) { 33 | $list[] = $v; 34 | } 35 | } 36 | $model->$attribute = $list; 37 | } elseif ($this->checkRequired($model, $attribute, $value)) { 38 | $model->$attribute = null; 39 | } 40 | } 41 | 42 | /** 43 | * @param ActiveRecord $model 44 | * @param string $attribute 45 | * @param Model|array $value 46 | * @return bool 47 | */ 48 | protected function checkRequired($model, $attribute, $value) 49 | { 50 | $value = $this->castValue($model, $attribute, $value); 51 | 52 | if ($value instanceof Model) { 53 | $value->clearErrors(); 54 | if ($value->beforeValidate()) { 55 | foreach ($value->rules() as $rule) { 56 | if ($this->isRequiredValidator($value, $rule)) { 57 | $validator = $rule instanceof Validator 58 | ? $rule 59 | : Validator::createValidator($rule[1], $value, (array)$rule[0], array_slice($rule, 2)); 60 | $validator->validateAttributes($value, $rule[0]); 61 | } 62 | } 63 | $value->afterValidate(); 64 | } 65 | 66 | return $value->hasErrors(); 67 | } 68 | 69 | return false; 70 | } 71 | 72 | /** 73 | * @param Model $model 74 | * @param Validator|array $rule 75 | * @return bool 76 | */ 77 | protected function isRequiredValidator($model, $rule) 78 | { 79 | if ($rule instanceof Validator) { 80 | return $rule instanceof RequiredValidator; 81 | } elseif (is_array($rule) && isset($rule[1])) { 82 | $type = $rule[1]; 83 | if ($type instanceof \Closure) { 84 | return false; 85 | } 86 | if (is_string($type)) { 87 | if ($type == 'required') { 88 | return true; 89 | } 90 | if (isset(static::$builtInValidators[$type]) || $model->hasMethod($type)) { 91 | return false; 92 | } 93 | } 94 | 95 | return is_a(is_array($type) ? $type['class'] : $type, RequiredValidator::class, true); 96 | } 97 | 98 | return false; 99 | } 100 | } -------------------------------------------------------------------------------- /src/validators/CompositeValidator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CompositeValidator extends AbstractCompositeValidator 15 | { 16 | /** @var bool Assign value after validation */ 17 | public $assignValue = false; 18 | 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function init() 23 | { 24 | parent::init(); 25 | if ($this->message === null) { 26 | $this->message = Yii::t('yii', '{attribute} is invalid.'); 27 | } 28 | } 29 | 30 | /** 31 | * @inheritdoc 32 | * @param ActiveRecord $model the model being validated 33 | */ 34 | public function validateAttribute($model, $attribute) 35 | { 36 | $value = $this->castValue($model, $attribute, $model->$attribute); 37 | if ($value instanceof Model) { 38 | if (!$value->validate()) { 39 | foreach ($value->getErrors() as $errors) { 40 | foreach ($errors as $error) { 41 | $model->addError($attribute, $error); 42 | } 43 | } 44 | } 45 | if ($this->assignValue) { 46 | $model->$attribute = $value; 47 | } 48 | } 49 | } 50 | } --------------------------------------------------------------------------------