diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 68fcd9a7e..1a783cf36 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: toolmantim/release-drafter@master + - uses: toolmantim/release-drafter@v5.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e725bcc65..adf7a2f34 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -15,14 +15,15 @@ jobs: container: image: atk4/image:${{ matrix.php }} # https://github.com/atk4/image strategy: + fail-fast: false matrix: php: ['7.2', '7.3', 'latest'] services: mysql: - image: mysql:5.7 + image: mariadb:10.5.1 env: MYSQL_ROOT_PASSWORD: password - DB_DATABASE: db + MYSQL_DATABASE: db options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 postgres: image: postgres:10-alpine @@ -31,7 +32,7 @@ jobs: POSTGRES_USER: postgres options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - run: php --version - name: Get Composer Cache Directory id: composer-cache @@ -49,7 +50,7 @@ jobs: - name: Run Tests run: | mkdir -p build/logs - mysql -uroot -ppassword -h mysql -e 'CREATE DATABASE db;' + # mysql -uroot -ppassword -h mysql -e 'CREATE DATABASE db;' - name: SQLite Testing run: vendor/bin/phpunit --configuration phpunit.xml --coverage-text --exclude-group dns diff --git a/composer.json b/composer.json index 4b90bdc0d..100147465 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,8 @@ "homepage": "https://nearly.guru/" } ], + "minimum-stability": "dev", + "prefer-stable": true, "require": { "php": ">=7.2.0", "ext-intl": "*", diff --git a/docs/advanced.rst b/docs/advanced.rst index 1d3c76cb8..4fd9f8b60 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -77,7 +77,7 @@ Another scenario which could benefit by type substitution would be:: ATK Data allow class substitution during load and iteration by breaking "afterLoad" hook. Place the following inside Transaction::init():: - $this->addHook('afterLoad', function ($m) { + $this->onHook('afterLoad', function ($m) { if (get_class($this) != $m->getClassName()) { $cl = '\\'.$this->getClassName(); $cl = new $cl($this->persistence); @@ -98,7 +98,7 @@ of the record. Finally to help with performance, you can implement a switch:: .. if ($this->typeSubstitution) { - $this->addHook('afterLoad', + $this->onHook('afterLoad', .......... ) } @@ -166,7 +166,7 @@ which I want to define like this:: $this->owner->addField('updated_dts', ['type'=>'datetime']); - $this->owner->addHook('beforeUpdate', function($m, $data) { + $this->owner->onHook('beforeUpdate', function($m, $data) { if(isset($this->app->user) and $this->app->user->loaded()) { $data['updated_by'] = $this->app->user->id; } @@ -343,7 +343,7 @@ before and just slightly modifying it:: $this->owner->addMethod('restore', $this); } else { $this->owner->addCondition('is_deleted', false); - $this->owner->addHook('beforeDelete', [$this, 'softDelete'], null, 100); + $this->owner->onHook('beforeDelete', [$this, 'softDelete'], null, 100); } } @@ -425,7 +425,7 @@ inside your model are unique:: $this->fields = [$this->owner->title_field]; } - $this->owner->addHook('beforeSave', $this); + $this->owner->onHook('beforeSave', $this); } function beforeSave($m) @@ -448,6 +448,33 @@ As expected - when you add a new model the new values are checked against existing records. You can also slightly modify the logic to make addCondition additive if you are verifying for the combination of matched fields. +Using WITH cursors +================== + +Many SQL database engines support defining WITH cursors to use in select, update +and even delete statements. + +.. php:method:: addWith(Model $model, string $alias, array $fields, bool $recursive = false) + + Agile toolkit data models also support these cursors. Usage is like this:: + + $invoices = new Invoice(); + + $contacts = new Contact(); + $contacts->addWith($invoices, 'inv', ['contact_id'=>'cid', 'ref_no', 'total_net'=>'invoiced'], false); + $contacts->join('inv.cid'); + +.. code-block:: sql + + with + `inv` (`cid`, `ref_no`, `total_net`) as (select `contact_id`, `ref_no`, `total_net` from `invoice`) + select + * + from `contact` + join `inv` on `inv`.`cid`=`contact`.`id` + +.. note:: Supported starting from MySQL 8.x. MariaDB supported it earlier. + Creating Many to Many relationship ================================== @@ -488,7 +515,7 @@ Next we need to define reference. Inside Model_Invoice add:: $j->hasOne('invoice_id', 'Model_Invoice'); }, 'their_field'=>'invoice_id']); - $this->addHook('beforeDelete',function($m){ + $this->onHook('beforeDelete',function($m){ $m->ref('InvoicePayment')->action('delete')->execute(); // If you have important per-row hooks in InvoicePayment @@ -623,7 +650,7 @@ Here is how to add them. First you need to create fields:: I have declared those fields with never_persist so they will never be used by persistence layer to load or save anything. Next I need a beforeSave handler:: - $this->addHook('beforeSave', function($m) { + $this->onHook('beforeSave', function($m) { if(isset($m['client_code']) && !isset($m['client_id'])) { $cl = $this->refModel('client_id'); $cl->addCondition('code',$m['client_code']); @@ -712,7 +739,7 @@ section. Add this into your Invoice Model:: Next both payment and lines need to be added after invoice is actually created, so:: - $this->addHook('afterSave', function($m, $is_update){ + $this->onHook('afterSave', function($m, $is_update){ if(isset($m['payment'])) { $m->ref('Payment')->insert($m['payment']); } diff --git a/docs/design.rst b/docs/design.rst index 5634aefa0..1138bbbdc 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -338,7 +338,7 @@ Hooks can help you perform operations when object is being persisted:: // addField() declaration // addExpression('is_password_expired') - $this->addHook('beforeSave', function($m) { + $this->onHook('beforeSave', function($m) { if ($m->isDirty('password')) { $m['password'] = encrypt_password($m['password']); $m['password_change_date'] = $m->expr('now()'); diff --git a/docs/hooks.rst b/docs/hooks.rst index bd504f472..ae54f97a5 100644 --- a/docs/hooks.rst +++ b/docs/hooks.rst @@ -52,7 +52,7 @@ Example with beforeSave The next code snippet demonstrates a basic usage of a `beforeSave` hook. This one will update field values just before record is saved:: - $m->addHook('beforeSave', function($m) { + $m->onHook('beforeSave', function($m) { $m['name'] = strtoupper($m['name']); $m['surname'] = strtoupper($m['surname']); }); @@ -85,7 +85,7 @@ model will assume the operation was successful. You can also break beforeLoad hook which can be used to skip rows:: - $model->addHook('afterLoad', function ($m) { + $model->onHook('afterLoad', function ($m) { if ($m['date'] < $m->date_from) { $m->breakHook(false); // will not yield such data row } @@ -136,7 +136,7 @@ of save. You may actually drop validation exception inside save, insert or update hooks:: - $m->addHook('beforeSave', function($m) { + $m->onHook('beforeSave', function($m) { if ($m['name'] = 'Yagi') { throw new \atk4\data\ValidationException(['name'=>"We don't serve like you"]); } @@ -193,7 +193,7 @@ and your update() may not actually update anything. This does not normally generate an error, however if you want to actually make sure that update() was effective, you can implement this through a hook:: - $m->addHook('afterUpdateQuery',function($m, $update, $st) { + $m->onHook('afterUpdateQuery',function($m, $update, $st) { if (!$st->rowCount()) { throw new \atk4\core\Exception([ 'Update didn\'t affect any records', @@ -213,7 +213,7 @@ In some cases you want to prevent default actions from executing. Suppose you want to check 'memcache' before actually loading the record from the database. Here is how you can implement this functionality:: - $m->addHook('beforeLoad',function($m, $id) { + $m->onHook('beforeLoad',function($m, $id) { $data = $m->app->cacheFetch($m->table, $id); if ($data) { $m->data = $data; @@ -239,13 +239,13 @@ This can be used in various situations. Save information into auditLog about failure: - $m->addHook('onRollback', function($m){ + $m->onHook('onRollback', function($m){ $m->auditLog->registerFailure(); }); Upgrade schema: - $m->addHook('onRollback', function($m, $exception) { + $m->onHook('onRollback', function($m, $exception) { if ($exception instanceof \PDOException) { $m->schema->upgrade(); $m->breakHook(false); // exception will not be thrown diff --git a/docs/model.rst b/docs/model.rst index b0ad99326..3e2203153 100644 --- a/docs/model.rst +++ b/docs/model.rst @@ -187,7 +187,7 @@ To invoke code from `init()` methods of ALL models (for example soft-delete logi you use Persistence's "afterAdd" hook. This will not affect ALL models but just models which are associated with said persistence:: - $db->addHook('afterAdd', function($p, $m) use($acl) { + $db->onHook('afterAdd', function($p, $m) use($acl) { $fields = $m->getFields(); @@ -391,7 +391,7 @@ a hook:: $this->addField('name'); - $this->addHook('validate', function($m) { + $this->onHook('validate', function($m) { if ($m['name'] == 'C#') { return ['name'=>'No sharp objects are allowed']; } @@ -437,7 +437,7 @@ action - `send_gift`. There are some advanced techniques like "SubTypes" or class substitution, for example, this hook may be placed in the "User" class init():: - $this->addHook('afterLoad', function($m) { + $this->onHook('afterLoad', function($m) { if ($m['purchases'] > 1000) { $this->breakHook($this->asModel(VIPUser::class); } @@ -740,6 +740,10 @@ Title Field Return title field value of currently loaded record. +.. php:method:: public getTitles + + Returns array of title field values of all model records in format [id => title]. + .. _caption: Model Caption diff --git a/docs/overview.rst b/docs/overview.rst index 0e9d02524..d58422c8b 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -166,7 +166,7 @@ If your persistence does not support expressions (e.g. you are using Redis or MongoDB), you would need to define the field differently:: $model->addField('gross'); - $model->addHook('beforeSave', function($m) { + $model->onHook('beforeSave', function($m) { $m['gross'] = $m['net'] + $m['vat']; }); @@ -186,7 +186,7 @@ you want it to work with NoSQL, then your solution might be:: // persistence does not support expressions $model->addField('gross'); - $model->addHook('beforeSave', function($m) { + $model->onHook('beforeSave', function($m) { $m['gross'] = $m['net'] + $m['vat']; }); diff --git a/docs/persistence.rst b/docs/persistence.rst index bcbe482c3..4a87bc8fb 100644 --- a/docs/persistence.rst +++ b/docs/persistence.rst @@ -496,7 +496,7 @@ this ref, how do you do it? Start by creating a beforeSave handler for Order:: - $this->addHook('beforeSave', function($m) { + $this->onHook('beforeSave', function($m) { if ($this->isDirty('ref')) { if ( @@ -684,11 +684,11 @@ application:: $m = $m->withPersistence($this->mdb)->replace(); } - $m->addHook('beforeSave', function($m){ + $m->onHook('beforeSave', function($m){ $m->withPersistence($this->sql)->save(); }); - $m->addHook('beforeDelete', function($m){ + $m->onHook('beforeDelete', function($m){ $m->withPersistence($this->sql)->delete(); }); @@ -729,11 +729,11 @@ records. The last two hooks are in order to replicate any changes into the SQL database also:: - $m->addHook('beforeSave', function($m){ + $m->onHook('beforeSave', function($m){ $m->withPersistence($this->sql)->save(); }); - $m->addHook('beforeDelete', function($m){ + $m->onHook('beforeDelete', function($m){ $m->withPersistence($this->sql)->delete(); }); @@ -784,7 +784,7 @@ Archive Copies into different persistence If you wish that every time you save your model the copy is also stored inside some other database (for archive purposes) you can implement it like this:: - $m->addHook('beforeSave', function($m) { + $m->onHook('beforeSave', function($m) { $arc = $this->withPersistence($m->app->archive_db, false); // add some audit fields diff --git a/src/Action/Iterator.php b/src/Action/Iterator.php index b66166673..0ab73bae0 100644 --- a/src/Action/Iterator.php +++ b/src/Action/Iterator.php @@ -38,7 +38,7 @@ public function where($field, $value) $this->generator = new \CallbackFilterIterator($this->generator, function ($row) use ($field, $value) { // skip row. does not have field at all - if (!isset($row[$field])) { + if (!array_key_exists($field, $row)) { return false; } @@ -66,12 +66,13 @@ public function like($field, $value) $this->generator = new \CallbackFilterIterator($this->generator, function ($row) use ($field, $value) { // skip row. does not have field at all - if (!isset($row[$field])) { + if (!array_key_exists($field, $row)) { return false; } - $clean_value = trim(trim($value), '%'); - // the row field exists check the position of th "%"(s) + $value = trim($value); + $clean_value = trim($value, '%'); + // the row field exists check the position of the "%"(s) switch ($value) { // case "%str%" case substr($value, -1, 1) == '%' && substr($value, 0, 1) == '%': @@ -85,6 +86,9 @@ public function like($field, $value) case substr($value, 0, 1) == '%': return substr($row[$field], -strlen($clean_value)) === $clean_value; break; + // full match + default: + return $row[$field] == $clean_value; } return false; diff --git a/src/Field.php b/src/Field.php index 6386bcf04..2e2e01966 100644 --- a/src/Field.php +++ b/src/Field.php @@ -12,6 +12,8 @@ /** * Class description? + * + * @property Model $owner */ class Field implements Expressionable { @@ -57,6 +59,10 @@ class Field implements Expressionable /** * If value of this field can be described by a model, this property * will contain reference to that model. + * + * It's used more in atk4/ui repository. See there. + * + * @var Reference|null */ public $reference = null; @@ -183,20 +189,20 @@ class Field implements Expressionable /** * DateTime class used for type = 'data', 'datetime', 'time' fields. * - * For example, 'DateTime', 'Carbon' etc. + * For example, 'DateTime', 'Carbon\Carbon' etc. * - * @param string + * @var string */ - public $dateTimeClass = 'DateTime'; + public $dateTimeClass = \DateTime::class; /** * Timezone class used for type = 'data', 'datetime', 'time' fields. * - * For example, 'DateTimeZone', 'Carbon' etc. + * For example, 'DateTimeZone', 'Carbon\CarbonTimeZone' etc. * - * @param string + * @var string */ - public $dateTimeZoneClass = 'DateTimeZone'; + public $dateTimeZoneClass = \DateTimeZone::class; // }}} @@ -330,7 +336,7 @@ public function normalize($value) case 'datetime': case 'time': // we allow http://php.net/manual/en/datetime.formats.relative.php - $class = isset($f->dateTimeClass) ? $f->dateTimeClass : 'DateTime'; + $class = $f->dateTimeClass ?? \DateTime::class; if (is_numeric($value)) { $value = new $class('@'.$value); @@ -399,10 +405,8 @@ public function normalize($value) * Casts field value to string. * * @param mixed $value Optional value - * - * @return string */ - public function toString($value = null) + public function toString($value = null): string { $v = ($value === null ? $this->get() : $this->normalize($value)); @@ -458,10 +462,8 @@ public function get() * Sets field value. * * @param mixed $value - * - * @return $this */ - public function set($value) + public function set($value): self { $this->owner->set($this->short_name, $value); @@ -473,20 +475,16 @@ public function set($value) * use examples. * * @param mixed $value - * - * @return bool */ - public function compare($value) + public function compare($value): bool { return $this->owner[$this->short_name] == $value; } /** * Should this field use alias? - * - * @return bool */ - public function useAlias() + public function useAlias(): bool { return isset($this->actual); } @@ -497,42 +495,32 @@ public function useAlias() /** * Returns if field should be editable in UI. - * - * @return bool */ - public function isEditable() + public function isEditable(): bool { - return isset($this->ui['editable']) ? $this->ui['editable'] - : (($this->read_only || $this->never_persist) ? false - : !$this->system); + return $this->ui['editable'] ?? !$this->read_only && !$this->never_persist && !$this->system; } /** * Returns if field should be visible in UI. - * - * @return bool */ - public function isVisible() + public function isVisible(): bool { - return isset($this->ui['visible']) ? $this->ui['visible'] : !$this->system; + return $this->ui['visible'] ?? !$this->system; } /** * Returns if field should be hidden in UI. - * - * @return bool */ - public function isHidden() + public function isHidden(): bool { - return isset($this->ui['hidden']) ? $this->ui['hidden'] : false; + return $this->ui['hidden'] ?? false; } /** * Returns field caption for use in UI. - * - * @return string */ - public function getCaption() + public function getCaption(): string { return $this->caption ?? $this->ui['caption'] ?? $this->readableCaption($this->short_name); } @@ -563,10 +551,8 @@ public function getDSQLExpression($expression) /** * Returns array with useful debug info for var_dump. - * - * @return array */ - public function __debugInfo() + public function __debugInfo(): array { $arr = [ 'short_name' => $this->short_name, diff --git a/src/Field/Boolean.php b/src/Field/Boolean.php index 20736ed89..497b434d1 100644 --- a/src/Field/Boolean.php +++ b/src/Field/Boolean.php @@ -91,10 +91,8 @@ public function normalize($value) * Casts field value to string. * * @param mixed $value Optional value - * - * @return string */ - public function toString($value = null) + public function toString($value = null): string { $v = ($value === null ? $this->get() : $this->normalize($value)); diff --git a/src/Field/Callback.php b/src/Field/Callback.php index 365591e54..ab776edf4 100644 --- a/src/Field/Callback.php +++ b/src/Field/Callback.php @@ -43,7 +43,9 @@ public function init() { $this->_init(); - $this->owner->addHook('afterLoad', function ($m) { + $this->ui['table']['sortable'] = false; + + $this->owner->onHook('afterLoad', function ($m) { $m->data[$this->short_name] = call_user_func($this->expr, $m); }); } diff --git a/src/Field_SQL.php b/src/Field_SQL.php index 20861cdde..3ea1809cd 100644 --- a/src/Field_SQL.php +++ b/src/Field_SQL.php @@ -9,6 +9,8 @@ /** * Class description? + * + * @property Join\SQL $join */ class Field_SQL extends Field implements Expressionable { diff --git a/src/Field_SQL_Expression.php b/src/Field_SQL_Expression.php index cf34e40b1..9742eda2d 100644 --- a/src/Field_SQL_Expression.php +++ b/src/Field_SQL_Expression.php @@ -61,7 +61,7 @@ public function init() } if ($this->concat) { - $this->owner->addHook('afterSave', $this); + $this->owner->onHook('afterSave', $this); } } @@ -69,7 +69,7 @@ public function init() * Possibly that user will attempt to insert values here. If that is the case, then * we would need to inject it into related hasMany relationship. * - * @param $m + * @param Model $m */ public function afterSave($m) { @@ -78,10 +78,8 @@ public function afterSave($m) /** * Should this field use alias? * Expression fields always need alias. - * - * @return bool */ - public function useAlias() + public function useAlias(): bool { return true; } @@ -97,8 +95,7 @@ public function getDSQLExpression($expression) { $expr = $this->expr; if (is_callable($expr)) { - $c = $this->expr; - $expr = $c($this->owner, $expression); + $expr = $expr($this->owner, $expression); } if (is_string($expr)) { diff --git a/src/Join.php b/src/Join.php index a988eec36..078905cbb 100644 --- a/src/Join.php +++ b/src/Join.php @@ -10,6 +10,8 @@ /** * Class description? + * + * @property Model $owner */ class Join { @@ -160,7 +162,8 @@ public function init() $this->_init(); // handle foreign table containing a dot - if (is_string($this->foreign_table) + if ( + is_string($this->foreign_table) && strpos($this->foreign_table, '.') !== false ) { if (!isset($this->reverse)) { @@ -180,14 +183,14 @@ public function init() ]); /* } - $this->reverse = 'link'; - - */ + */ } } - list($this->foreign_table, $this->foreign_field) = - explode('.', $this->foreign_table, 2); + + // split by LAST dot in foreign_table name + list($this->foreign_table, $this->foreign_field) = preg_split('/\.+(?=[^\.]+$)/', $this->foreign_table); + if (!$this->master_field) { $this->master_field = 'id'; } @@ -203,7 +206,7 @@ public function init() } } - $this->owner->addHook('afterUnload', $this); + $this->owner->onHook('afterUnload', $this); } /** diff --git a/src/Join/Array_.php b/src/Join/Array_.php index ac7367a45..22bcf8dd0 100644 --- a/src/Join/Array_.php +++ b/src/Join/Array_.php @@ -27,14 +27,14 @@ public function init() // Add necessary hooks if ($this->reverse) { - $this->owner->addHook('afterInsert', $this, null, -5); - $this->owner->addHook('beforeUpdate', $this, null, -5); - $this->owner->addHook('beforeDelete', [$this, 'doDelete'], null, -5); + $this->owner->onHook('afterInsert', $this, [], -5); + $this->owner->onHook('beforeUpdate', $this, [], -5); + $this->owner->onHook('beforeDelete', [$this, 'doDelete'], [], -5); } else { - $this->owner->addHook('beforeInsert', $this); - $this->owner->addHook('beforeUpdate', $this); - $this->owner->addHook('afterDelete', [$this, 'doDelete']); - $this->owner->addHook('afterLoad', $this); + $this->owner->onHook('beforeInsert', $this); + $this->owner->onHook('beforeUpdate', $this); + $this->owner->onHook('afterDelete', [$this, 'doDelete']); + $this->owner->onHook('afterLoad', $this); } } diff --git a/src/Join/SQL.php b/src/Join/SQL.php index f690b9a7c..b70975053 100644 --- a/src/Join/SQL.php +++ b/src/Join/SQL.php @@ -9,6 +9,9 @@ /** * Join\SQL class. + * + * @property \atk4\data\Persistence\SQL $persistence + * @property SQL $join */ class SQL extends Join implements \atk4\dsql\Expressionable { @@ -69,17 +72,17 @@ public function init() // Our short name will be unique if (!$this->foreign_alias) { - $this->foreign_alias = (isset($this->owner->table_alias) ? $this->owner->table_alias : '').$this->short_name; + $this->foreign_alias = ($this->owner->table_alias ?: '').$this->short_name; } - $this->owner->addhook('initSelectQuery', $this); + $this->owner->onHook('initSelectQuery', $this); // Add necessary hooks if ($this->reverse) { - $this->owner->addHook('afterInsert', $this); - $this->owner->addHook('beforeUpdate', $this); - $this->owner->addHook('beforeDelete', [$this, 'doDelete'], null, -5); - $this->owner->addHook('afterLoad', $this); + $this->owner->onHook('afterInsert', $this); + $this->owner->onHook('beforeUpdate', $this); + $this->owner->onHook('beforeDelete', [$this, 'doDelete'], [], -5); + $this->owner->onHook('afterLoad', $this); } else { // Master field indicates ID of the joined item. In the past it had to be @@ -99,10 +102,10 @@ public function init() } } - $this->owner->addHook('beforeInsert', $this, null, -5); - $this->owner->addHook('beforeUpdate', $this); - $this->owner->addHook('afterDelete', [$this, 'doDelete']); - $this->owner->addHook('afterLoad', $this); + $this->owner->onHook('beforeInsert', $this, [], -5); + $this->owner->onHook('beforeUpdate', $this); + $this->owner->onHook('afterDelete', [$this, 'doDelete']); + $this->owner->onHook('afterLoad', $this); } } @@ -131,29 +134,31 @@ public function initSelectQuery($model, $query) // if ON is set, we don't have to worry about anything if ($this->on) { $query->join( - $this->foreign_table.' '.$this->foreign_alias, + $this->foreign_table, $this->on instanceof \atk4\dsql\Expression ? $this->on : $model->expr($this->on), - $this->kind + $this->kind, + $this->foreign_alias ); return; } $query->join( - $this->foreign_table.(isset($this->foreign_alias) ? (' '.$this->foreign_alias) : ''), - $model->expr('{}.{} = {}', [ - (isset($this->foreign_alias) ? $this->foreign_alias : $this->foreign_table), + $this->foreign_table, + $model->expr('{{}}.{} = {}', [ + ($this->foreign_alias ?: $this->foreign_table), $this->foreign_field, $this->owner->getField($this->master_field), ]), - $this->kind + $this->kind, + $this->foreign_alias ); /* if ($this->reverse) { $query->field([$this->short_name => ($this->join ?: ( - (isset($this->owner->table_alias) ? $this->owner->table_alias : $this->owner->table) + ($this->owner->table_alias ?: $this->owner->table) .'.'.$this->master_field) )]); } else { diff --git a/src/Model.php b/src/Model.php index 74c86dd66..98d5ad274 100644 --- a/src/Model.php +++ b/src/Model.php @@ -4,7 +4,6 @@ namespace atk4\data; -use ArrayAccess; use atk4\core\AppScopeTrait; use atk4\core\CollectionTrait; use atk4\core\ContainerTrait; @@ -17,12 +16,13 @@ use atk4\core\ReadableCaptionTrait; use atk4\data\UserAction\Generic; use atk4\dsql\Query; -use IteratorAggregate; /** * Data model class. + * + * @property Field[]|Reference[] $elements */ -class Model implements ArrayAccess, IteratorAggregate +class Model implements \ArrayAccess, \IteratorAggregate { use ContainerTrait { add as _add; @@ -185,6 +185,13 @@ class Model implements ArrayAccess, IteratorAggregate */ public $order = []; + /** + * Array of WITH cursors set. + * + * @var array + */ + public $with = []; + /** * Currently loaded record data. This record is associative array * that contain field=>data pairs. It may contain data for un-defined @@ -220,7 +227,7 @@ class Model implements ArrayAccess, IteratorAggregate * SECURITY WARNING: If you are looking for a RELIABLE way to restrict access * to model data, please check Secure Enclave extension. * - * @param bool + * @var bool */ public $read_only = false; @@ -518,13 +525,13 @@ public function fieldFactory($seed = []) ); /** @var Field $field */ - $field = $this->factory($seed, null, '\atk4\data\Field'); + $field = $this->factory($seed, null, Field::class); return $field; } protected $typeToFieldSeed = [ - 'boolean' => ['Boolean'], + 'boolean' => [Field\Boolean::class], ]; /** @@ -938,6 +945,20 @@ public function getTitle() return $f ? $f->get() : $this->id; } + /** + * Returns array of model record titles [id => title]. + * + * @return array + */ + public function getTitles() + { + $field = $this->title_field && $this->hasField($this->title_field) ? $this->title_field : $this->id_field; + + return array_map(function ($row) use ($field) { + return $row[$field]; + }, $this->export([$field], $this->id_field)); + } + /** * You can compare new value of the field with existing one without * retrieving. In the trivial case it's same as ($value == $model[$name]) @@ -1108,8 +1129,7 @@ public function getAction($name): UserAction\Generic /** * Execute specified action with specified arguments. * - * @param $name - * @param $args + * @param string $name Action name * * @throws Exception * @throws \atk4\core\Exception @@ -1157,7 +1177,7 @@ public function removeAction($name) * ->addCondition('my_field', '!=', $value); * ->addCondition('my_field', 'in', [$value1, $value2]); * - * Second argument could be '=', '>', '<', '>=', '<=', '!=' or 'in'. + * Second argument could be '=', '>', '<', '>=', '<=', '!=', 'in', 'like' or 'regexp'. * Those conditions are still supported by most of persistence drivers. * * There are also vendor-specific expression support: @@ -1207,7 +1227,7 @@ public function addCondition($field, $operator = null, $value = null) $f = is_string($field) ? $this->getField($field) : ($field instanceof Field ? $field : false); if ($f) { if ($operator === '=' || func_num_args() == 2) { - $v = $operator === '=' ? $value : $operator; + $v = ($operator === '=' ? $value : $operator); if (!is_object($v) && !is_array($v)) { $f->system = true; @@ -1233,6 +1253,31 @@ public function withID($id) return $this->addCondition($this->id_field, $id); } + /** + * Adds WITH cursor. + * + * @param Model $model + * @param string $alias + * @param array $mapping + * @param bool $recursive + * + * @return $this + */ + public function addWith(self $model, string $alias, array $mapping = [], bool $recursive = false) + { + if (isset($this->with[$alias])) { + throw new Exception(['With cursor already set with this alias', 'alias'=>$alias]); + } + + $this->with[$alias] = [ + 'model' => $model, + 'mapping' => $mapping, + 'recursive' => $recursive, + ]; + + return $this; + } + /** * Set order for model records. Multiple calls. * @@ -2077,12 +2122,12 @@ public function getIterator() foreach ($this->rawIterator() as $data) { $this->data = $this->persistence->typecastLoadRow($this, $data); if ($this->id_field) { - $this->id = isset($data[$this->id_field]) ? $data[$this->id_field] : null; + $this->id = $data[$this->id_field] ?? null; } // you can return false in afterLoad hook to prevent to yield this data row // use it like this: - // $model->addHook('afterLoad', function ($m) { + // $model->onHook('afterLoad', function ($m) { // if ($m['date'] < $m->date_from) $m->breakHook(false); // }) @@ -2582,10 +2627,8 @@ public function lastInsertID() /** * Returns array with useful debug info for var_dump. - * - * @return array */ - public function __debugInfo() + public function __debugInfo(): array { $arr = [ 'id' => $this->id, diff --git a/src/Persistence.php b/src/Persistence.php index 438a38822..6fadf4403 100644 --- a/src/Persistence.php +++ b/src/Persistence.php @@ -40,7 +40,7 @@ public static function connect($dsn, $user = null, $password = null, $args = []) // Process DSN string $dsn = \atk4\dsql\Connection::normalizeDSN($dsn, $user, $password); - $driver = isset($args['driver']) ? strtolower($args['driver']) : $dsn['driver']; + $driver = strtolower($args['driver'] ?? $dsn['driver']); switch ($driver) { case 'mysql': diff --git a/src/Persistence/Array_.php b/src/Persistence/Array_.php index 3dba9aa84..be9dfb327 100644 --- a/src/Persistence/Array_.php +++ b/src/Persistence/Array_.php @@ -49,7 +49,7 @@ public function add($m, $defaults = []) } $defaults = array_merge([ - '_default_seed_join' => 'atk4\data\Join\Array_', + '_default_seed_join' => \atk4\data\Join\Array_::class, ], $defaults); $m = parent::add($m, $defaults); @@ -202,7 +202,7 @@ public function update(Model $m, $id, $data, $table = null) $this->data[$table][$id] = array_merge( - isset($this->data[$table][$id]) ? $this->data[$table][$id] : [], + $this->data[$table][$id] ?? [], $data ); @@ -330,8 +330,8 @@ protected function setLimitOrder($m, &$action) // then set limit if ($m->limit && ($m->limit[0] || $m->limit[1])) { - $cnt = isset($m->limit[0]) ? $m->limit[0] : 0; - $shift = isset($m->limit[1]) ? $m->limit[1] : 0; + $cnt = $m->limit[0] ?? 0; + $shift = $m->limit[1] ?? 0; $action->limit($cnt, $shift); } @@ -359,7 +359,7 @@ public function applyConditions(Model $model, \atk4\data\Action\Iterator $iterat array_splice($cond, -1, 1, ['where', $cond[1]]); } - // condition must have 3 params at these point + // condition must have 3 params at this point if (count($cond) != 3) { // condition can have up to three params throw new Exception([ @@ -369,39 +369,37 @@ public function applyConditions(Model $model, \atk4\data\Action\Iterator $iterat ]); } - // action + // extract + $field = $cond[0]; $method = strtolower($cond[1]); + $value = $cond[2]; // check if the method is supported by the iterator if (!method_exists($iterator, $method)) { throw new Exception([ 'Persistence\Array_ driver condition unsupported method', - 'reason' => "method $method not implemented for Action\Iterator", - 'condition'=> $cond, + 'reason' => "method $method not implemented for Action\Iterator", + 'condition' => $cond, ]); } // get the model field - if (is_string($cond[0])) { - $cond[0] = $model->getField($cond[0]); + if (is_string($field)) { + $field = $model->getField($field); } - if (!is_a($cond[0], \atk4\data\Field::class)) { + if (!is_a($field, Field::class)) { throw new Exception([ 'Persistence\Array_ driver condition unsupported format', - 'reason' => 'Unsupported object instance '.get_class($cond[0]), - 'condition' => [ - get_class($cond[0]), - $cond[1], - $cond[2], - ], + 'reason' => 'Unsupported object instance '.get_class($field), + 'condition' => $cond, ]); } // get the field name - $short_name = $cond[0]->short_name; + $short_name = $field->short_name; // .. the value - $value = $this->typecastSaveField($cond[0], $cond[2]); + $value = $this->typecastSaveField($field, $value); // run the (filter) method $iterator->{$method}($short_name, $value); } @@ -427,14 +425,14 @@ public function action($m, $type, $args = []) switch ($type) { case 'select': - $action = $this->initAction($m, isset($args[0]) ? $args[0] : null); + $action = $this->initAction($m, $args[0] ?? null); $this->applyConditions($m, $action); $this->setLimitOrder($m, $action); return $action; case 'count': - $action = $this->initAction($m, isset($args[0]) ? $args[0] : null); + $action = $this->initAction($m, $args[0] ?? null); $this->applyConditions($m, $action); $this->setLimitOrder($m, $action); diff --git a/src/Persistence/SQL.php b/src/Persistence/SQL.php index 759a19057..5436e2c4a 100644 --- a/src/Persistence/SQL.php +++ b/src/Persistence/SQL.php @@ -30,35 +30,35 @@ class SQL extends Persistence * * @var string */ - public $_default_seed_addField = ['\atk4\data\Field_SQL']; + public $_default_seed_addField = \atk4\data\Field_SQL::class; /** * Default class when adding hasOne field. * * @var string */ - public $_default_seed_hasOne = ['\atk4\data\Reference\HasOne_SQL']; + public $_default_seed_hasOne = \atk4\data\Reference\HasOne_SQL::class; /** * Default class when adding hasMany field. * * @var string */ - public $_default_seed_hasMany = null; //'atk4\data\Reference\HasMany'; + public $_default_seed_hasMany = null; // \atk4\data\Reference\HasMany::class; /** * Default class when adding Expression field. * * @var string */ - public $_default_seed_addExpression = ['\atk4\data\Field_SQL_Expression']; + public $_default_seed_addExpression = Field_SQL_Expression::class; /** * Default class when adding join. * * @var string */ - public $_default_seed_join = ['\atk4\data\Join\SQL']; + public $_default_seed_join = \atk4\data\Join\SQL::class; /** * Constructor. @@ -246,9 +246,45 @@ public function initQuery(Model $m): Query } } + // add With cursors + $this->initWithCursors($m, $d); + return $d; } + /** + * Initializes WITH cursors. + * + * @param Model $m + * @param Query $q + */ + public function initWithCursors(Model $m, Query $q) + { + if (!$m->with) { + return; + } + + foreach ($m->with as $alias => ['model'=>$model, 'mapping'=>$mapping, 'recursive'=>$recursive]) { + // prepare field names + $fields_from = $fields_to = []; + foreach ($mapping as $from => $to) { + $fields_from[] = is_int($from) ? $to : $from; + $fields_to[] = $to; + } + + // prepare sub-query + if ($fields_from) { + $model->onlyFields($fields_from); + } + // 2nd parameter here strictly define which fields should be selected + // as result system fields will not be added if they are not requested + $sub_q = $model->action('select', [$fields_from]); + + // add With cursor + $q->with($sub_q, $alias, $fields_to ?: null, $recursive); + } + } + /** * Adds Field in Query. * @@ -268,7 +304,7 @@ public function initField(Query $q, Field $field) * Adds model fields in Query. * * @param Model $m - * @param \atk4\dsql\Query $q + * @param Query $q * @param array|null|false $fields */ public function initQueryFields(Model $m, $q, $fields = null) @@ -374,7 +410,7 @@ public function initQueryConditions(Model $m, Query $q): Query // count($cond) == 1, we will pass the only // parameter inside where() - if (count($cond) == 1) { + if (count($cond) === 1) { // OR conditions if (is_array($cond[0])) { @@ -383,9 +419,9 @@ public function initQueryConditions(Model $m, Query $q): Query $row[0] = $m->getField($row[0]); } - if ($row[0] instanceof Field) { - $valueKey = count($row) == 2 ? 1 : 2; - + // "like" or "regexp" conditions do not need typecasting to field type! + if ($row[0] instanceof Field && (count($row) === 2 || !in_array(strtolower($row[1]), ['like', 'regexp']))) { + $valueKey = count($row) === 2 ? 1 : 2; $row[$valueKey] = $this->typecastSaveField($row[0], $row[$valueKey]); } } @@ -399,13 +435,14 @@ public function initQueryConditions(Model $m, Query $q): Query $cond[0] = $m->getField($cond[0]); } - if (count($cond) == 2) { + if (count($cond) === 2) { if ($cond[0] instanceof Field) { $cond[1] = $this->typecastSaveField($cond[0], $cond[1]); } $q->where($cond[0], $cond[1]); } else { - if ($cond[0] instanceof Field) { + // "like" or "regexp" conditions do not need typecasting to field type! + if ($cond[0] instanceof Field && !in_array(strtolower($cond[1]), ['like', 'regexp'])) { $cond[2] = $this->typecastSaveField($cond[0], $cond[2]); } $q->where($cond[0], $cond[1], $cond[2]); @@ -450,8 +487,8 @@ public function _typecastSaveField(Field $f, $value) case 'date': case 'datetime': case 'time': - $dt_class = isset($f->dateTimeClass) ? $f->dateTimeClass : 'DateTime'; - $tz_class = isset($f->dateTimeZoneClass) ? $f->dateTimeZoneClass : 'DateTimeZone'; + $dt_class = $f->dateTimeClass ?? \DateTime::class; + $tz_class = $f->dateTimeZoneClass ?? \DateTimeZone::class; if ($v instanceof $dt_class || $v instanceof \DateTimeInterface) { $format = ['date' => 'Y-m-d', 'datetime' => 'Y-m-d H:i:s.u', 'time' => 'H:i:s.u']; @@ -526,8 +563,8 @@ public function _typecastLoadField(Field $f, $value) case 'date': case 'datetime': case 'time': - $dt_class = isset($f->dateTimeClass) ? $f->dateTimeClass : 'DateTime'; - $tz_class = isset($f->dateTimeZoneClass) ? $f->dateTimeZoneClass : 'DateTimeZone'; + $dt_class = $f->dateTimeClass ?? \DateTime::class; + $tz_class = $f->dateTimeZoneClass ?? \DateTimeZone::class; if (is_numeric($v)) { $v = new $dt_class('@'.$v); @@ -584,7 +621,7 @@ public function _typecastLoadField(Field $f, $value) * @param string $type * @param array $args * - * @return \atk4\dsql\Query + * @return Query */ public function action(Model $m, $type, $args = []) { @@ -613,7 +650,7 @@ public function action(Model $m, $type, $args = []) return $q; case 'select': - $this->initQueryFields($m, $q, isset($args[0]) ? $args[0] : null); + $this->initQueryFields($m, $q, $args[0] ?? null); break; case 'count': @@ -718,6 +755,7 @@ public function tryLoad(Model $m, $id) throw new Exception([ 'Unable to load due to query error', 'query' => $load->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -785,6 +823,7 @@ public function tryLoadAny(Model $m) throw new Exception([ 'Unable to load due to query error', 'query' => $load->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -862,6 +901,7 @@ public function insert(Model $m, $data) throw new Exception([ 'Unable to execute insert query', 'query' => $insert->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -911,6 +951,7 @@ public function prepareIterator(Model $m) throw new Exception([ 'Unable to execute iteration query', 'query' => $export->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -950,6 +991,7 @@ public function update(Model $m, $id, $data) throw new Exception([ 'Unable to update due to query error', 'query' => $update->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -994,6 +1036,7 @@ public function delete(Model $m, $id) throw new Exception([ 'Unable to delete due to query error', 'query' => $delete->getDebugQuery(false), + 'message' => $e->getMessage(), 'model' => $m, 'conditions' => $m->conditions, ], null, $e); @@ -1003,15 +1046,11 @@ public function delete(Model $m, $id) public function getFieldSQLExpression(Field $field, Expression $expression) { if (isset($field->owner->persistence_data['use_table_prefixes'])) { - $mask = '{}.{}'; + $mask = '{{}}.{}'; $prop = [ $field->join - ? (isset($field->join->foreign_alias) - ? $field->join->foreign_alias - : $field->join->short_name) - : (isset($field->owner->table_alias) - ? $field->owner->table_alias - : $field->owner->table), + ? ($field->join->foreign_alias ?: $field->join->short_name) + : ($field->owner->table_alias ?: $field->owner->table), $field->actual ?: $field->short_name, ]; } else { diff --git a/src/Persistence/Static_.php b/src/Persistence/Static_.php index eee33b08a..8b9f6fd55 100644 --- a/src/Persistence/Static_.php +++ b/src/Persistence/Static_.php @@ -47,7 +47,7 @@ public function __construct($data = null) // chomp off first row, we will use it to deduct fields $row1 = reset($data); - $this->addHook('afterAdd', [$this, 'afterAdd']); + $this->onHook('afterAdd', [$this, 'afterAdd']); if (!is_array($row1)) { // We are dealing with array of strings. Convert it into array of hashes diff --git a/src/Reference.php b/src/Reference.php index 86e70aad5..9e8644218 100644 --- a/src/Reference.php +++ b/src/Reference.php @@ -10,6 +10,8 @@ * getModel() and that's pretty much it. * * It's possible to extend the basic reference with more meaningful references. + * + * @property Model $owner */ class Reference { @@ -20,14 +22,6 @@ class Reference use \atk4\core\DIContainerTrait; use \atk4\core\FactoryTrait; - /** - * Owner Model of the reference. - * override the hint type definition already present in TrackableTrait. - * - * @var Model - */ - public $owner; - /** * Use this alias for related entity by default. This can help you * if you create sub-queries or joins to separate this from main @@ -251,10 +245,8 @@ public function refModel($defaults = []): Model /** * Returns array with useful debug info for var_dump. - * - * @return array */ - public function __debugInfo() + public function __debugInfo(): array { $arr = []; foreach ($this->__debug_fields as $k => $v) { diff --git a/src/Reference/ContainsMany.php b/src/Reference/ContainsMany.php index a49d82301..62ae579ce 100644 --- a/src/Reference/ContainsMany.php +++ b/src/Reference/ContainsMany.php @@ -66,7 +66,7 @@ public function ref($defaults = []): Model ])); // set some hooks for ref_model - $m->addHook(['afterSave', 'afterDelete'], function ($model) { + $m->onHook(['afterSave', 'afterDelete'], function ($model) { $rows = $model->persistence->data[$this->table_alias]; $this->owner->save([$this->our_field => $rows ?: null]); }); diff --git a/src/Reference/ContainsOne.php b/src/Reference/ContainsOne.php index 9ceb947b1..1861f6ce4 100644 --- a/src/Reference/ContainsOne.php +++ b/src/Reference/ContainsOne.php @@ -119,7 +119,7 @@ public function ref($defaults = []): Model ])); // set some hooks for ref_model - $m->addHook(['afterSave', 'afterDelete'], function ($model) { + $m->onHook(['afterSave', 'afterDelete'], function ($model) { $row = $model->persistence->data[$this->table_alias]; $row = $row ? array_shift($row) : null; // get first and only one record from array persistence $this->owner->save([$this->our_field => $row]); diff --git a/src/Reference/HasMany.php b/src/Reference/HasMany.php index 2a6799b61..d35ccfa94 100644 --- a/src/Reference/HasMany.php +++ b/src/Reference/HasMany.php @@ -38,8 +38,6 @@ protected function getOurValue() /** * Returns our field or id field. - * - * @return Field */ protected function referenceOurValue(): Field { @@ -54,8 +52,6 @@ protected function referenceOurValue(): Field * @param array $defaults Properties * * @throws Exception - * - * @return Model */ public function ref($defaults = []): Model { @@ -72,8 +68,6 @@ public function ref($defaults = []): Model * @param array $defaults Properties * * @throws Exception - * - * @return Model */ public function refLink($defaults = []): Model { @@ -92,8 +86,6 @@ public function refLink($defaults = []): Model * @param array $defaults Properties * * @throws Exception - * - * @return Field */ public function addField($n, $defaults = []): Field { @@ -107,8 +99,8 @@ public function addField($n, $defaults = []): Field $defaults['aggregate_relation'] = $this; - $field_n = isset($defaults['field']) ? $defaults['field'] : $n; - $field = isset($defaults['field']) ? $defaults['field'] : null; + $field_n = $defaults['field'] ?? $n; + $field = $defaults['field'] ?? null; if (isset($defaults['concat'])) { $defaults['aggregate'] = $this->owner->dsql()->groupConcat($field_n, $defaults['concat']); @@ -122,7 +114,7 @@ public function addField($n, $defaults = []): Field return $r->action('field', [$r->expr( $defaults['expr'], - isset($defaults['args']) ? $defaults['args'] : null + $defaults['args'] ?? null ), 'alias'=>$field]); }; unset($defaults['args']); diff --git a/src/Reference/HasOne.php b/src/Reference/HasOne.php index 0dfe6be04..02c8b820b 100644 --- a/src/Reference/HasOne.php +++ b/src/Reference/HasOne.php @@ -140,7 +140,7 @@ class HasOne extends Reference * * For example, 'DateTime', 'Carbon' etc. * - * @param string + * @var string */ public $dateTimeClass = 'DateTime'; @@ -149,7 +149,7 @@ class HasOne extends Reference * * For example, 'DateTimeZone', 'Carbon' etc. * - * @param string + * @var string */ public $dateTimeZoneClass = 'DateTimeZone'; @@ -222,7 +222,7 @@ public function ref($defaults = []): Model $m = $this->getModel($defaults); // add hook to set our_field = null when record of referenced model is deleted - $m->addHook('afterDelete', function ($m) { + $m->onHook('afterDelete', function ($m) { $this->owner[$this->our_field] = null; }); @@ -232,19 +232,19 @@ public function ref($defaults = []): Model $m->tryLoadBy($this->their_field, $this->owner[$this->our_field]); } - return - $m->addHook('afterSave', function ($m) { - $this->owner[$this->our_field] = $m[$this->their_field]; - }); - } - - if ($this->owner[$this->our_field]) { - $m->tryLoad($this->owner[$this->our_field]); - } + $m->onHook('afterSave', function ($m) { + $this->owner[$this->our_field] = $m[$this->their_field]; + }); + } else { + if ($this->owner[$this->our_field]) { + $m->tryLoad($this->owner[$this->our_field]); + } - return - $m->addHook('afterSave', function ($m) { + $m->onHook('afterSave', function ($m) { $this->owner[$this->our_field] = $m->id; }); + } + + return $m; } } diff --git a/src/Reference/HasOne_SQL.php b/src/Reference/HasOne_SQL.php index 311be39db..7e04faf64 100644 --- a/src/Reference/HasOne_SQL.php +++ b/src/Reference/HasOne_SQL.php @@ -52,12 +52,13 @@ public function addField($field, ?string $their_field = null): Field_SQL_Express $defaults['caption'] = $defaults['caption'] ?? $this->owner->refModel($this->link)->getField($their_field)->getCaption(); /** @var Field_SQL_Expression $e */ - $e = $this->owner->addExpression($field, array_merge([ - function (Model $m) use ($their_field) { - // remove order if we just select one field from hasOne model - // that is mandatory for Oracle - return $m->refLink($this->link)->action('field', [$their_field])->reset('order'); - }, ], + $e = $this->owner->addExpression($field, array_merge( + [ + function (Model $m) use ($their_field) { + // remove order if we just select one field from hasOne model + // that is mandatory for Oracle + return $m->refLink($this->link)->action('field', [$their_field])->reset('order'); + }, ], $defaults )); @@ -65,7 +66,7 @@ function (Model $m) use ($their_field) { $e->never_save = true; // Will try to execute last - $this->owner->addHook('beforeSave', function (Model $m) use ($field, $their_field) { + $this->owner->onHook('beforeSave', function (Model $m) use ($field, $their_field) { // if title field is changed, but reference ID field (our_field) // is not changed, then update reference ID field value if ($m->isDirty($field) && !$m->isDirty($this->our_field)) { @@ -75,7 +76,7 @@ function (Model $m) use ($their_field) { $m[$this->our_field] = $mm->action('field', [$mm->id_field]); unset($m[$field]); } - }, null, 21); + }, [], 21); return $e; } @@ -226,7 +227,7 @@ public function addTitle($defaults = []): Field_SQL_Expression } /** @var Field_SQL_Expression $ex */ - $ex = $this->owner->addExpression($field, array_merge_recursive( + $ex = $this->owner->addExpression($field, array_replace_recursive( [ function (Model $m) { $mm = $m->refLink($this->link); @@ -246,7 +247,7 @@ function (Model $m) { )); // Will try to execute last - $this->owner->addHook('beforeSave', function (Model $m) use ($field) { + $this->owner->onHook('beforeSave', function (Model $m) use ($field) { // if title field is changed, but reference ID field (our_field) // is not changed, then update reference ID field value if ($m->isDirty($field) && !$m->isDirty($this->our_field)) { @@ -255,7 +256,7 @@ function (Model $m) { $mm->addCondition($mm->title_field, $m[$field]); $m[$this->our_field] = $mm->action('field', [$mm->id_field]); } - }, null, 20); + }, [], 20); // Set ID field as not visible in grid by default if (!array_key_exists('visible', $this->owner->getField($this->our_field)->ui)) { diff --git a/src/UserAction/Generic.php b/src/UserAction/Generic.php index 7fc407ca3..6db7245d2 100644 --- a/src/UserAction/Generic.php +++ b/src/UserAction/Generic.php @@ -16,6 +16,8 @@ * action trigger (button) correctly in an automated way. * * Action must NOT rely on any specific UI implementation. + * + * @property Model $owner */ class Generic { @@ -25,9 +27,6 @@ class Generic init as init_; } - /** @var Model */ - public $owner; - /** Defining scope of the action */ const NO_RECORDS = 'none'; // e.g. add const SINGLE_RECORD = 'single'; // e.g. archive @@ -67,7 +66,7 @@ class Generic /** @var array Argument definition. */ public $args = []; - /** @var array|null Specify which fields may be dirty when invoking action. NO_RECORDS|SINGLE_RECORD scopes for adding/modifying */ + /** @var array|bool Specify which fields may be dirty when invoking action. NO_RECORDS|SINGLE_RECORD scopes for adding/modifying */ public $fields = []; /** @var bool Atomic action will automatically begin transaction before and commit it after completing. */ @@ -111,9 +110,9 @@ public function execute(...$args) 'permitted' => $this->fields, ]); } - } elseif ($this->fields !== false) { + } elseif (!is_bool($this->fields)) { throw new Exception([ - 'Argument `fields` for the action must be either array or `false`.', + 'Argument `fields` for the action must be either array or boolean.', 'fields'=> $this->fields, ]); } diff --git a/src/Util/DeepCopy.php b/src/Util/DeepCopy.php index 4ca3a5a11..09e888ad8 100644 --- a/src/Util/DeepCopy.php +++ b/src/Util/DeepCopy.php @@ -147,12 +147,8 @@ public function transformData($transforms) /** * Will extract non-numeric keys from the array. - * - * @param $array - * - * @return array */ - protected function extractKeys($array): array + protected function extractKeys(array $array): array { $result = []; foreach ($array as $key=>$val) { @@ -177,12 +173,12 @@ protected function extractKeys($array): array public function copy() { return $this->_copy( - $this->source, - $this->destination, - $this->references, - $this->exclusions, - $this->transforms - )->reload(); + $this->source, + $this->destination, + $this->references, + $this->exclusions, + $this->transforms + )->reload(); } /** diff --git a/tests/ConditionSQLTest.php b/tests/ConditionSQLTest.php index 25ef1e5c9..e2c3b434f 100644 --- a/tests/ConditionSQLTest.php +++ b/tests/ConditionSQLTest.php @@ -48,6 +48,33 @@ public function testBasic() $this->assertEquals('Sue', $mm['name']); } + public function testNull() + { + $a = [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John', 'gender' => 'M'], + 2 => ['id' => 2, 'name' => 'Sue', 'gender' => 'F'], + 3 => ['id' => 3, 'name' => 'Null1', 'gender' => null], + 4 => ['id' => 4, 'name' => 'Null2', 'gender' => null], + ], ]; + $this->setDB($a); + + $m = new Model($this->db, 'user'); + $m->addFields(['name', 'gender']); + + $m->addCondition('gender', null); + + $nullCount = 0; + foreach ($m as $user) { + $this->assertNull($user['gender']); + $this->assertContains('Null', $user['name']); + + $nullCount++; + } + + $this->assertEquals(2, $nullCount); + } + public function testOperations() { $a = [ @@ -373,4 +400,39 @@ public function testLoadBy() $this->assertFalse($u->getField('name')->system); // should not set field as system $this->assertNull($u->getField('name')->default); // should not set field default value } + + /** + * Test LIKE condition. + */ + public function testLikeCondition() + { + $a = [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John', 'active' => 1, 'created' => '2020-01-01 15:00:30'], + 2 => ['id' => 2, 'name' => 'Peter', 'active' => 0, 'created' => '2019-05-20 12:13:14'], + 3 => ['id' => 3, 'name' => 'Joe', 'active' => 1, 'created' => '2019-07-15 09:55:05'], + ], + ]; + $this->setDB($a); + + $u = new Model($this->db, 'user'); + $u->addField('name', ['type' => 'string']); + $u->addField('active', ['type' => 'boolean']); + $u->addField('created', ['type' => 'datetime']); + + $t = (clone $u)->addCondition('created', 'like', '%19%'); + $this->assertEquals(2, count($t->export())); // only year 2019 records + + $t = (clone $u)->addCondition('active', 'like', '%1%'); + $this->assertEquals(2, count($t->export())); // only active records + + $t = (clone $u)->addCondition('active', 'like', '%0%'); + $this->assertEquals(1, count($t->export())); // only inactive records + + $t = (clone $u)->addCondition('active', 'like', '%999%'); + $this->assertEquals(0, count($t->export())); // bad value, so it will not match anything + + $t = (clone $u)->addCondition('active', 'like', '%ABC%'); + $this->assertEquals(0, count($t->export())); // bad value, so it will not match anything + } } diff --git a/tests/ContainsManyTest.php b/tests/ContainsManyTest.php index 2fed0598b..3e5ab47bf 100644 --- a/tests/ContainsManyTest.php +++ b/tests/ContainsManyTest.php @@ -131,8 +131,8 @@ public function setUp() parent::setUp(); // populate database for our models - $this->getMigration(new VatRate2($this->db))->drop()->create(); - $this->getMigration(new Invoice2($this->db))->drop()->create(); + $this->getMigrator(new VatRate2($this->db))->drop()->create(); + $this->getMigrator(new Invoice2($this->db))->drop()->create(); // fill in some default values $m = new VatRate2($this->db); @@ -269,13 +269,13 @@ public function testNestedContainsMany() $this->assertEquals( json_encode([ '1' => [ - 'id' => 1, 'vat_rate_id' => 1, 'price' => '10', 'qty' => '2', 'add_date' => (new \DateTime('2019-06-01'))->format('Y-m-d\TH:i:sP'), 'discounts' => json_encode([ + 'id' => 1, 'vat_rate_id' => '1', 'price' => '10', 'qty' => '2', 'add_date' => (new \DateTime('2019-06-01'))->format('Y-m-d\TH:i:sP'), 'discounts' => json_encode([ '1' => ['id' => 1, 'percent' => '5', 'valid_till' => (new \DateTime('2019-07-15'))->format('Y-m-d\TH:i:sP')], '2' => ['id' => 2, 'percent' => '10', 'valid_till' => (new \DateTime('2019-07-30'))->format('Y-m-d\TH:i:sP')], ]), ], '2' => [ - 'id' => 2, 'vat_rate_id' => 2, 'price' => '15', 'qty' => '5', 'add_date' => (new \DateTime('2019-07-01'))->format('Y-m-d\TH:i:sP'), 'discounts' => json_encode([ + 'id' => 2, 'vat_rate_id' => '2', 'price' => '15', 'qty' => '5', 'add_date' => (new \DateTime('2019-07-01'))->format('Y-m-d\TH:i:sP'), 'discounts' => json_encode([ '1' => ['id' => 1, 'percent' => '20', 'valid_till' => (new \DateTime('2019-12-31'))->format('Y-m-d\TH:i:sP')], ]), ], diff --git a/tests/ContainsOneTest.php b/tests/ContainsOneTest.php index 675266d78..db1a5878e 100644 --- a/tests/ContainsOneTest.php +++ b/tests/ContainsOneTest.php @@ -96,8 +96,8 @@ public function setUp() parent::setUp(); // populate database for our models - $this->getMigration(new Country1($this->db))->drop()->create(); - $this->getMigration(new Invoice1($this->db))->drop()->create(); + $this->getMigrator(new Country1($this->db))->drop()->create(); + $this->getMigrator(new Invoice1($this->db))->drop()->create(); // fill in some default values $m = new Country1($this->db); @@ -172,7 +172,7 @@ public function testContainsOne() // let's test how it all looks in persistence without typecasting $exp_addr = $i->export(null, null, false)[0]['addr']; $this->assertEquals( - '{"country_id":2,"address":"bar","built_date":"2019-01-01T00:00:00+00:00","tags":"[\"foo\",\"bar\"]","door_code":"{\"code\":\"DEF\",\"valid_till\":\"2019-07-01T00:00:00+00:00\"}"}', + '{"country_id":"2","address":"bar","built_date":"2019-01-01T00:00:00+00:00","tags":"[\"foo\",\"bar\"]","door_code":"{\"code\":\"DEF\",\"valid_till\":\"2019-07-01T00:00:00+00:00\"}"}', $exp_addr ); diff --git a/tests/DeepCopyTest.php b/tests/DeepCopyTest.php index 87a4c788e..8456b272d 100644 --- a/tests/DeepCopyTest.php +++ b/tests/DeepCopyTest.php @@ -43,7 +43,7 @@ public function init() $this->addField('is_paid', ['type'=>'boolean', 'default'=>false]); - $this->addHook('afterCopy', function ($m, $s) { + $this->onHook('afterCopy', function ($m, $s) { if (get_class($s) == get_class($this)) { $m['ref'] = $m['ref'].'_copy'; } @@ -140,11 +140,11 @@ public function setUp() parent::setUp(); // populate database for our three models - $this->getMigration(new DCClient($this->db))->drop()->create(); - $this->getMigration(new DCInvoice($this->db))->drop()->create(); - $this->getMigration(new DCQuote($this->db))->drop()->create(); - $this->getMigration(new DCInvoiceLine($this->db))->drop()->create(); - $this->getMigration(new DCPayment($this->db))->drop()->create(); + $this->getMigrator(new DCClient($this->db))->drop()->create(); + $this->getMigrator(new DCInvoice($this->db))->drop()->create(); + $this->getMigrator(new DCQuote($this->db))->drop()->create(); + $this->getMigrator(new DCInvoiceLine($this->db))->drop()->create(); + $this->getMigrator(new DCPayment($this->db))->drop()->create(); } public function testBasic() @@ -275,7 +275,7 @@ public function testError() $quote->loadAny(); $invoice = new DCInvoice(); - $invoice->addHook('afterCopy', function ($m) { + $invoice->onHook('afterCopy', function ($m) { if (!$m['ref']) { throw new \atk4\core\Exception('no ref'); } @@ -317,7 +317,7 @@ public function testDeepError() $quote->loadAny(); $invoice = new DCInvoice(); - $invoice->addHook('afterCopy', function ($m) { + $invoice->onHook('afterCopy', function ($m) { if (!$m['ref']) { throw new \atk4\core\Exception('no ref'); } diff --git a/tests/FieldTest.php b/tests/FieldTest.php index 183fdafc0..a2aba6aea 100644 --- a/tests/FieldTest.php +++ b/tests/FieldTest.php @@ -339,7 +339,7 @@ public function testPersist() $a['item'][1]['surname'] = 'Stalker'; $this->assertEquals($a, $this->getDB()); - $m->addHook('beforeSave', function ($m) { + $m->onHook('beforeSave', function ($m) { if ($m->isDirty('name')) { $m['surname'] = $m['name']; unset($m['name']); diff --git a/tests/JoinArrayTest.php b/tests/JoinArrayTest.php index a052cd2d2..f611f6839 100644 --- a/tests/JoinArrayTest.php +++ b/tests/JoinArrayTest.php @@ -258,15 +258,17 @@ public function testJoinUpdate() $m_u['contact_phone'] = '+555'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'Joe', 'contact_id' => 2], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+321'], - ], ], $a + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'Joe', 'contact_id' => 2], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+321'], + ], ], + $a ); $m_u->load(3); @@ -274,15 +276,17 @@ public function testJoinUpdate() $m_u['contact_phone'] = '+999'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+999'], - ], ], $a + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+999'], + ], ], + $a ); $m_u->tryLoad(4); @@ -290,17 +294,19 @@ public function testJoinUpdate() $m_u['contact_phone'] = '+777'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+999'], - 3 => ['id' => 3, 'contact_phone' => '+777'], - ], ], $a + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+999'], + 3 => ['id' => 3, 'contact_phone' => '+777'], + ], ], + $a ); } @@ -327,15 +333,17 @@ public function testJoinDelete() $m_u->load(1); $m_u->delete(); - $this->assertEquals([ - 'user' => [ - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], - ], 'contact' => [ - 2 => ['id' => 2, 'contact_phone' => '+999'], - 3 => ['id' => 3, 'contact_phone' => '+777'], - ], ], $a + $this->assertEquals( + [ + 'user' => [ + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], + ], 'contact' => [ + 2 => ['id' => 2, 'contact_phone' => '+999'], + 3 => ['id' => 3, 'contact_phone' => '+777'], + ], ], + $a ); } diff --git a/tests/JoinSQLTest.php b/tests/JoinSQLTest.php index 368e92de2..29538768c 100644 --- a/tests/JoinSQLTest.php +++ b/tests/JoinSQLTest.php @@ -258,15 +258,17 @@ public function testJoinUpdate() $m_u['contact_phone'] = '+555'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'Joe', 'contact_id' => 2], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+321'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'Joe', 'contact_id' => 2], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+321'], + ], ], + $this->getDB() ); $m_u->load(1); @@ -276,29 +278,33 @@ public function testJoinUpdate() $m_u['name'] = 'XX'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+321'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+321'], + ], ], + $this->getDB() ); $m_u['contact_phone'] = '+999'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+999'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+999'], + ], ], + $this->getDB() ); $m_u->tryLoad(4); @@ -306,17 +312,19 @@ public function testJoinUpdate() $m_u['contact_phone'] = '+777'; $m_u->save(); - $this->assertEquals([ - 'user' => [ - 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], - ], 'contact' => [ - 1 => ['id' => 1, 'contact_phone' => '+555'], - 2 => ['id' => 2, 'contact_phone' => '+999'], - 3 => ['id' => 3, 'contact_phone' => '+777'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 1 => ['id' => 1, 'name' => 'John 2', 'contact_id' => 1], + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], + ], 'contact' => [ + 1 => ['id' => 1, 'contact_phone' => '+555'], + 2 => ['id' => 2, 'contact_phone' => '+999'], + 3 => ['id' => 3, 'contact_phone' => '+777'], + ], ], + $this->getDB() ); } @@ -345,15 +353,17 @@ public function testJoinDelete() $m_u->load(1); $m_u->delete(); - $this->assertEquals([ - 'user' => [ - 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], - 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], - 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], - ], 'contact' => [ - 2 => ['id' => 2, 'contact_phone' => '+999'], - 3 => ['id' => 3, 'contact_phone' => '+777'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 2 => ['id' => 2, 'name' => 'Peter', 'contact_id' => 1], + 3 => ['id' => 3, 'name' => 'XX', 'contact_id' => 2], + 4 => ['id' => 4, 'name' => 'YYY', 'contact_id' => 3], + ], 'contact' => [ + 2 => ['id' => 2, 'contact_phone' => '+999'], + 3 => ['id' => 3, 'contact_phone' => '+777'], + ], ], + $this->getDB() ); } @@ -373,7 +383,7 @@ public function testDoubleSaveHook() $j = $m_u->join('contact.test_id'); $j->addField('contact_phone'); - $m_u->addHook('afterSave', function ($m) { + $m_u->onHook('afterSave', function ($m) { if ($m['contact_phone'] != '+123') { $m['contact_phone'] = '+123'; $m->save(); @@ -438,22 +448,24 @@ public function testDoubleJoin() $m_u->unload(); $m_u->save(['name' => 'new', 'contact_phone' => '+000', 'country_name' => 'LV']); - $this->assertEquals([ - 'user' => [ - 20 => ['id' => 20, 'name' => 'Peter', 'contact_id' => 100], - 30 => ['id' => 30, 'name' => 'XX', 'contact_id' => 200], - 40 => ['id' => 40, 'name' => 'YYY', 'contact_id' => 300], - 41 => ['id' => 41, 'name' => 'new', 'contact_id' => 301], - ], 'contact' => [ - 200 => ['id' => 200, 'contact_phone' => '+999', 'country_id' => 2], - 300 => ['id' => 300, 'contact_phone' => '+777', 'country_id' => 5], - 301 => ['id' => 301, 'contact_phone' => '+000', 'country_id' => 4], - ], 'country' => [ - - 2 => ['id' => 2, 'name' => 'USA'], - 3 => ['id' => 3, 'name' => 'India'], - 4 => ['id' => 4, 'name' => 'LV'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 20 => ['id' => 20, 'name' => 'Peter', 'contact_id' => 100], + 30 => ['id' => 30, 'name' => 'XX', 'contact_id' => 200], + 40 => ['id' => 40, 'name' => 'YYY', 'contact_id' => 300], + 41 => ['id' => 41, 'name' => 'new', 'contact_id' => 301], + ], 'contact' => [ + 200 => ['id' => 200, 'contact_phone' => '+999', 'country_id' => 2], + 300 => ['id' => 300, 'contact_phone' => '+777', 'country_id' => 5], + 301 => ['id' => 301, 'contact_phone' => '+000', 'country_id' => 4], + ], 'country' => [ + + 2 => ['id' => 2, 'name' => 'USA'], + 3 => ['id' => 3, 'name' => 'India'], + 4 => ['id' => 4, 'name' => 'LV'], + ], ], + $this->getDB() ); } @@ -492,19 +504,21 @@ public function testDoubleReverseJoin() $m_u->loadBy('country_name', 'US'); $this->assertEquals(30, $m_u->id); - $this->assertEquals([ - 'user' => [ - 20 => ['id' => 20, 'name' => 'Peter', 'contact_id' => 100], - 30 => ['id' => 30, 'name' => 'XX', 'contact_id' => 200], - 40 => ['id' => 40, 'name' => 'YYY', 'contact_id' => 300], - ], 'contact' => [ - 200 => ['id' => 200, 'contact_phone' => '+999', 'country_id' => 2], - 300 => ['id' => 300, 'contact_phone' => '+777', 'country_id' => 5], - ], 'country' => [ - - 2 => ['id' => 2, 'name' => 'US'], - 3 => ['id' => 3, 'name' => 'India'], - ], ], $this->getDB() + $this->assertEquals( + [ + 'user' => [ + 20 => ['id' => 20, 'name' => 'Peter', 'contact_id' => 100], + 30 => ['id' => 30, 'name' => 'XX', 'contact_id' => 200], + 40 => ['id' => 40, 'name' => 'YYY', 'contact_id' => 300], + ], 'contact' => [ + 200 => ['id' => 200, 'contact_phone' => '+999', 'country_id' => 2], + 300 => ['id' => 300, 'contact_phone' => '+777', 'country_id' => 5], + ], 'country' => [ + + 2 => ['id' => 2, 'name' => 'US'], + 3 => ['id' => 3, 'name' => 'India'], + ], ], + $this->getDB() ); } diff --git a/tests/LookupSQLTest.php b/tests/LookupSQLTest.php index 5301c52ac..712d04af6 100644 --- a/tests/LookupSQLTest.php +++ b/tests/LookupSQLTest.php @@ -102,7 +102,7 @@ public function init() // add or remove reverse friendships /* - $this->addHook('afterInsert', function($m) { + $this->onHook('afterInsert', function($m) { if ($m->skip_reverse) { return; } @@ -115,7 +115,7 @@ public function init() ]); }); - $this->addHook('beforeDelete', function($m) { + $this->onHook('beforeDelete', function($m) { if ($m->skip_reverse) { return; } @@ -160,9 +160,9 @@ public function setUp() parent::setUp(); // populate database for our three models - $this->getMigration(new LCountry($this->db))->drop()->create(); - $this->getMigration(new LUser($this->db))->drop()->create(); - $this->getMigration(new LFriend($this->db))->drop()->create(); + $this->getMigrator(new LCountry($this->db))->drop()->create(); + $this->getMigrator(new LUser($this->db))->drop()->create(); + $this->getMigrator(new LFriend($this->db))->drop()->create(); } /** diff --git a/tests/PersistentArrayTest.php b/tests/PersistentArrayTest.php index 0daef6f54..c534ffb13 100644 --- a/tests/PersistentArrayTest.php +++ b/tests/PersistentArrayTest.php @@ -336,21 +336,22 @@ public function testActionField() public function testLike() { $a = ['countries' => [ - 1 => ['id'=>1, 'name'=>'ABC9', 'code'=>11, 'country'=>'Ireland'], - 2 => ['id'=>2, 'name'=>'ABC8', 'code'=>12, 'country'=>'Ireland'], - 3 => ['id'=>3, 'code'=>13, 'country'=>'Latvia'], - 4 => ['id'=>4, 'name'=>'ABC6', 'code'=>14, 'country'=>'UK'], - 5 => ['id'=>5, 'name'=>'ABC5', 'code'=>15, 'country'=>'UK'], - 6 => ['id'=>6, 'name'=>'ABC4', 'code'=>16, 'country'=>'Ireland'], - 7 => ['id'=>7, 'name'=>'ABC3', 'code'=>17, 'country'=>'Latvia'], - 8 => ['id'=>8, 'name'=>'ABC2', 'code'=>18, 'country'=>'Russia'], - 9 => ['id'=>9, 'code'=>19, 'country'=>'Latvia'], + 1 => ['id'=>1, 'name'=>'ABC9', 'code'=>11, 'country'=>'Ireland', 'active'=>1], + 2 => ['id'=>2, 'name'=>'ABC8', 'code'=>12, 'country'=>'Ireland', 'active'=>0], + 3 => ['id'=>3, 'code'=>13, 'country'=>'Latvia', 'active'=>1], + 4 => ['id'=>4, 'name'=>'ABC6', 'code'=>14, 'country'=>'UK', 'active'=>0], + 5 => ['id'=>5, 'name'=>'ABC5', 'code'=>15, 'country'=>'UK', 'active'=>0], + 6 => ['id'=>6, 'name'=>'ABC4', 'code'=>16, 'country'=>'Ireland', 'active'=>1], + 7 => ['id'=>7, 'name'=>'ABC3', 'code'=>17, 'country'=>'Latvia', 'active'=>0], + 8 => ['id'=>8, 'name'=>'ABC2', 'code'=>18, 'country'=>'Russia', 'active'=>1], + 9 => ['id'=>9, 'code'=>19, 'country'=>'Latvia', 'active'=>1], ]]; $p = new Persistence\Array_($a); $m = new Model($p, 'countries'); $m->addField('code', ['type' => 'int']); $m->addField('country'); + $m->addField('active', ['type' => 'boolean']); // if no condition we should get all the data back $iterator = $m->action('select'); @@ -396,6 +397,31 @@ public function testLike() $this->assertEquals($a['countries'][9], $result[9]); unset($result); $m->unload(); + + // case : boolean field + $m->conditions = []; + $m->addCondition('active', 'LIKE', '0'); + $this->assertEquals(4, count($m->export())); + + $m->conditions = []; + $m->addCondition('active', 'LIKE', '1'); + $this->assertEquals(5, count($m->export())); + + $m->conditions = []; + $m->addCondition('active', 'LIKE', '%0%'); + $this->assertEquals(4, count($m->export())); + + $m->conditions = []; + $m->addCondition('active', 'LIKE', '%1%'); + $this->assertEquals(5, count($m->export())); + + $m->conditions = []; + $m->addCondition('active', 'LIKE', '%999%'); + $this->assertEquals(0, count($m->export())); + + $m->conditions = []; + $m->addCondition('active', 'LIKE', '%ABC%'); + $this->assertEquals(0, count($m->export())); } /** diff --git a/tests/RandomTest.php b/tests/RandomTest.php index e33027cc2..4ac8f407f 100644 --- a/tests/RandomTest.php +++ b/tests/RandomTest.php @@ -25,7 +25,7 @@ public function init() { parent::init(); $this->addField('name'); - $this->hasOne('parent_item_id', '\atk4\data\tests\Model_Item') + $this->hasOne('parent_item_id', self::class) ->addTitle(); } } @@ -270,7 +270,7 @@ public function testUpdateCondition() $m->addField('name'); $m->load(2); - $m->addHook('afterUpdateQuery', function ($m, $update, $st) { + $m->onHook('afterUpdateQuery', function ($m, $update, $st) { // we can use afterUpdate to make sure that record was updated @@ -319,17 +319,17 @@ public function testHookBreakers() $m = new Model($db, 'user'); $m->addField('name'); - $m->addHook('beforeSave', function ($m) { + $m->onHook('beforeSave', function ($m) { $m->breakHook(false); }); - $m->addHook('beforeLoad', function ($m, $id) { + $m->onHook('beforeLoad', function ($m, $id) { $m->data = ['name' => 'rec #'.$id]; $m->id = $id; $m->breakHook(false); }); - $m->addHook('beforeDelete', function ($m, $id) { + $m->onHook('beforeDelete', function ($m, $id) { $m->unload(); $m->breakHook(false); }); @@ -350,7 +350,7 @@ public function testIssue220() $db = new Persistence\SQL($this->db->connection); $m = new Model_Item($db); - $m->hasOne('foo', '\atk4\data\tests\Model_Item') + $m->hasOne('foo', Model_Item::class) ->addTitle(); // field foo already exists, so we can't add title with same name } @@ -414,9 +414,11 @@ public function testGetTitle() // default title_field = name $this->assertEquals(null, $m->getTitle()); // not loaded model returns null + $this->assertEquals([1=>'John', 2=>'Sue'], $m->getTitles()); // all titles $m->load(2); $this->assertEquals('Sue', $m->getTitle()); // loaded returns title_field value + $this->assertEquals([1=>'John', 2=>'Sue'], $m->getTitles()); // all titles // set custom title_field $m->title_field = 'parent_item_id'; @@ -435,6 +437,7 @@ public function testGetTitle() $m->title_field = 'my_name'; $m->load(2); $this->assertEquals(2, $m->getTitle()); // loaded returns id value + $this->assertEquals([1=>1, 2=>2], $m->getTitles()); // all titles (my_name) } /** @@ -528,6 +531,31 @@ public function testNewInstance() $a = $m->newInstance(); $this->assertTrue(isset($a->persistence)); } + + public function testTableNameDots() + { + $d = new Model($this->db, 'db2.doc'); + $d->addField('name'); + + $m = new Model($this->db, 'db1.user'); + $m->addField('name'); + + $d->hasOne('user_id', $m)->addTitle(); + $m->hasMany('Documents', $d); + + $d->addCondition('user', 'Sarah'); + + $q = 'select "id","name","user_id",(select "name" from "db1"."user" where "id" = "db2"."doc"."user_id") "user" from "db2"."doc" where (select "name" from "db1"."user" where "id" = "db2"."doc"."user_id") = :a'; + $q = str_replace('"', $this->getEscapeChar(), $q); + $this->assertEquals( + $q, + $d->action('select')->render() + ); + } +} + +class CustomField extends \atk4\data\Field +{ } class CustomField extends \atk4\data\Field diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index b56f8f083..d4dd19c66 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -15,24 +15,36 @@ public function testBasicSerialize() $f = $m->addField('data', ['serialize' => 'serialize']); $this->assertEquals( - ['data' => 'a:1:{s:3:"foo";s:3:"bar";}'], $db->typecastSaveRow($m, - ['data' => ['foo' => 'bar']] - )); + ['data' => 'a:1:{s:3:"foo";s:3:"bar";}'], + $db->typecastSaveRow( + $m, + ['data' => ['foo' => 'bar']] + ) + ); $this->assertEquals( - ['data' => ['foo' => 'bar']], $db->typecastLoadRow($m, - ['data' => 'a:1:{s:3:"foo";s:3:"bar";}'] - )); + ['data' => ['foo' => 'bar']], + $db->typecastLoadRow( + $m, + ['data' => 'a:1:{s:3:"foo";s:3:"bar";}'] + ) + ); $f->serialize = 'json'; $f->type = 'array'; $this->assertEquals( - ['data' => '{"foo":"bar"}'], $db->typecastSaveRow($m, - ['data' => ['foo' => 'bar']] - )); + ['data' => '{"foo":"bar"}'], + $db->typecastSaveRow( + $m, + ['data' => ['foo' => 'bar']] + ) + ); $this->assertEquals( - ['data' => ['foo' => 'bar']], $db->typecastLoadRow($m, - ['data' => '{"foo":"bar"}'] - )); + ['data' => ['foo' => 'bar']], + $db->typecastLoadRow( + $m, + ['data' => '{"foo":"bar"}'] + ) + ); } /** diff --git a/tests/SubTypesTest.php b/tests/SubTypesTest.php index 7f1a396df..2f0b4b1ad 100644 --- a/tests/SubTypesTest.php +++ b/tests/SubTypesTest.php @@ -74,7 +74,7 @@ public function init() } $this->addField('amount'); - $this->addHook('afterLoad', function (self $m) { + $this->onHook('afterLoad', function (self $m) { if (get_class($this) != $m->getClassName()) { $cl = '\\'.$this->getClassName(); $cl = new $cl($this->persistence); @@ -87,7 +87,7 @@ public function init() public function getClassName() { - return 'atk4\data\tests\STTransaction_'.$this['type']; + return __NAMESPACE__.'\STTransaction_'.$this['type']; } } @@ -140,8 +140,8 @@ public function setUp() parent::setUp(); // populate database for our three models - $this->getMigration(new STAccount($this->db))->drop()->create(); - $this->getMigration(new STTransaction_TransferOut($this->db))->drop()->create(); + $this->getMigrator(new STAccount($this->db))->drop()->create(); + $this->getMigrator(new STTransaction_TransferOut($this->db))->drop()->create(); } public function testBasic() @@ -152,16 +152,16 @@ public function testBasic() $inheritance->transferTo($current, 500); $current->withdraw(350); - $this->assertEquals('atk4\data\tests\STTransaction_OB', get_class($inheritance->ref('Transactions')->load(1))); - $this->assertEquals('atk4\data\tests\STTransaction_TransferOut', get_class($inheritance->ref('Transactions')->load(2))); - $this->assertEquals('atk4\data\tests\STTransaction_TransferIn', get_class($current->ref('Transactions')->load(3))); - $this->assertEquals('atk4\data\tests\STTransaction_Withdrawal', get_class($current->ref('Transactions')->load(4))); + $this->assertEquals(STTransaction_OB::class, get_class($inheritance->ref('Transactions')->load(1))); + $this->assertEquals(STTransaction_TransferOut::class, get_class($inheritance->ref('Transactions')->load(2))); + $this->assertEquals(STTransaction_TransferIn::class, get_class($current->ref('Transactions')->load(3))); + $this->assertEquals(STTransaction_Withdrawal::class, get_class($current->ref('Transactions')->load(4))); $cl = []; foreach ($current->ref('Transactions') as $tr) { $cl[] = get_class($tr); } - $this->assertEquals(['atk4\data\tests\STTransaction_TransferIn', 'atk4\data\tests\STTransaction_Withdrawal'], $cl); + $this->assertEquals([STTransaction_TransferIn::class, STTransaction_Withdrawal::class], $cl); } } diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php index d3c35e06e..c0988d072 100644 --- a/tests/TransactionTest.php +++ b/tests/TransactionTest.php @@ -25,7 +25,7 @@ public function testAtomicOperations() $m->addField('name'); $m->load(2); - $m->addHook('afterSave', function ($m) { + $m->onHook('afterSave', function ($m) { throw new \Exception('Awful thing happened'); }); $m['name'] = 'XXX'; @@ -37,7 +37,7 @@ public function testAtomicOperations() $this->assertEquals('Sue', $this->getDB()['item'][2]['name']); - $m->addHook('afterDelete', function ($m) { + $m->onHook('afterDelete', function ($m) { throw new \Exception('Awful thing happened'); }); @@ -62,7 +62,7 @@ public function testBeforeSaveHook() // test insert $m = new Model($db, 'item'); $m->addField('name'); - $m->addHook('beforeSave', function ($model, $is_update) use ($self) { + $m->onHook('beforeSave', function ($model, $is_update) use ($self) { $self->assertFalse($is_update); }); $m->save(['name'=>'Foo']); @@ -70,7 +70,7 @@ public function testBeforeSaveHook() // test update $m = new Model($db, 'item'); $m->addField('name'); - $m->addHook('afterSave', function ($model, $is_update) use ($self) { + $m->onHook('afterSave', function ($model, $is_update) use ($self) { $self->assertTrue($is_update); }); $m->loadBy('name', 'John')->save(['name'=>'Foo']); @@ -89,7 +89,7 @@ public function testAfterSaveHook() // test insert $m = new Model($db, 'item'); $m->addField('name'); - $m->addHook('afterSave', function ($model, $is_update) use ($self) { + $m->onHook('afterSave', function ($model, $is_update) use ($self) { $self->assertFalse($is_update); }); $m->save(['name'=>'Foo']); @@ -97,7 +97,7 @@ public function testAfterSaveHook() // test update $m = new Model($db, 'item'); $m->addField('name'); - $m->addHook('afterSave', function ($model, $is_update) use ($self) { + $m->onHook('afterSave', function ($model, $is_update) use ($self) { $self->assertTrue($is_update); }); $m->loadBy('name', 'John')->save(['name'=>'Foo']); @@ -120,7 +120,7 @@ public function testOnRollbackHook() $hook_called = false; $values = []; - $m->addHook('onRollback', function ($mm, $e) use (&$hook_called, &$values) { + $m->onHook('onRollback', function ($mm, $e) use (&$hook_called, &$values) { $hook_called = true; $values = $mm->get(); // model field values are still the same no matter we rolled back $mm->breakHook(false); // if we break hook and return false then exception is not thrown, but rollback still happens diff --git a/tests/TypecastingTest.php b/tests/TypecastingTest.php index 637f0c00d..e63966867 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -265,9 +265,9 @@ public function testTypeCustom1() $m = new Model($db, ['table' => 'types']); - $m->addField('date', ['type' => 'date', 'dateTimeClass' => '\atk4\data\tests\MyDate']); - $m->addField('datetime', ['type' => 'datetime', 'dateTimeClass' => '\atk4\data\tests\MyDateTime']); - $m->addField('time', ['type' => 'time', 'dateTimeClass' => '\atk4\data\tests\MyTime']); + $m->addField('date', ['type' => 'date', 'dateTimeClass' => MyDate::class]); + $m->addField('datetime', ['type' => 'datetime', 'dateTimeClass' => MyDateTime::class]); + $m->addField('time', ['type' => 'time', 'dateTimeClass' => MyTime::class]); $m->addField('b1', ['type' => 'boolean', 'enum' => ['N', 'Y']]); $m->addField('b2', ['type' => 'boolean', 'enum' => ['N', 'Y']]); $m->addField('money', ['type' => 'money']); @@ -325,7 +325,7 @@ public function testTryLoad() $m = new Model($db, ['table' => 'types']); - $m->addField('date', ['type' => 'date', 'dateTimeClass' => '\atk4\data\tests\MyDate']); + $m->addField('date', ['type' => 'date', 'dateTimeClass' => MyDate::class]); $m->tryLoad(1); @@ -345,7 +345,7 @@ public function testTryLoadAny() $m = new Model($db, ['table' => 'types']); - $m->addField('date', ['type' => 'date', 'dateTimeClass' => '\atk4\data\tests\MyDate']); + $m->addField('date', ['type' => 'date', 'dateTimeClass' => MyDate::class]); $m->tryLoadAny(); @@ -365,7 +365,7 @@ public function testTryLoadBy() $m = new Model($db, ['table' => 'types']); - $m->addField('date', ['type' => 'date', 'dateTimeClass' => '\atk4\data\tests\MyDate']); + $m->addField('date', ['type' => 'date', 'dateTimeClass' => MyDate::class]); $m->loadBy('id', 1); @@ -384,7 +384,7 @@ public function testLoadBy() $db = new Persistence\SQL($this->db->connection); $m = new Model($db, ['table' => 'types']); - $m->addField('date', ['type' => 'date', 'dateTimeClass' => '\atk4\data\tests\MyDate']); + $m->addField('date', ['type' => 'date', 'dateTimeClass' => MyDate::class]); $m->loadAny(); $d = $m['date']; $m->unload(); diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 10e6462d9..54bf218f9 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -113,7 +113,7 @@ public function testValidate5() public function testValidateHook() { - $this->m->addHook('validate', function ($m) { + $this->m->onHook('validate', function ($m) { if ($m['name'] === 'C#') { return ['name'=>'No sharp objects allowed']; } diff --git a/tests/WithTest.php b/tests/WithTest.php new file mode 100644 index 000000000..be64e0936 --- /dev/null +++ b/tests/WithTest.php @@ -0,0 +1,62 @@ + [ + 10 => ['id' => 10, 'name' => 'John', 'salary' => 2500], + 20 => ['id' => 20, 'name' => 'Peter', 'salary' => 4000], + ], 'invoice' => [ + 1 => ['id' => 1, 'net' => 500, 'user_id' => 10], + 2 => ['id' => 2, 'net' => 200, 'user_id' => 20], + 3 => ['id' => 3, 'net' => 100, 'user_id' => 20], + ], ]; + $this->setDB($a); + $db = new Persistence\SQL($this->db->connection); + + // setup models + $m_user = new Model($db, 'user'); + $m_user->addField('name'); + $m_user->addField('salary', ['type'=>'money']); + + $m_invoice = new Model($db, 'invoice'); + $m_invoice->addField('net', ['type'=>'money']); + $m_invoice->hasOne('user_id', $m_user); + $m_invoice->addCondition('net', '>', 100); + + // setup test model + $m = clone $m_user; + $m->addWith($m_invoice, 'i', ['user_id', 'net'=>'invoiced']); // add cursor + $j_invoice = $m->join('i.user_id'); // join cursor + $j_invoice->addField('invoiced'); // add field from joined cursor + + // tests + $q = 'with "i" ("user_id","invoiced") as (select "user_id","net" from "invoice" where "net" > 100) select "user"."id","user"."name","user"."salary","_i"."invoiced" from "user" inner join "i" as "_i" on "_i"."user_id" = "user"."id"'; + $q = str_replace('"', $this->getEscapeChar(), $q); + $this->assertEquals($q, $m->action('select')->getDebugQuery()); + $this->assertEquals(2, count($m->export())); + } + + /** + * Alias should be unique. + * + * @expectedException Exception + */ + public function testUniqueAliasException() + { + $m1 = new Model(); + $m2 = new Model(); + $m1->addWith($m2, 't'); + $m1->addWith($m2, 't'); + } +} diff --git a/tests/smbo/SMBOTestCase.php b/tests/smbo/SMBOTestCase.php index d026ba3e5..121c22ef1 100644 --- a/tests/smbo/SMBOTestCase.php +++ b/tests/smbo/SMBOTestCase.php @@ -8,7 +8,7 @@ public function setUp() { parent::setUp(); - $s = $this->getMigration(); + $s = $this->getMigrator(); $x = clone $s; $x->table('account')->drop() diff --git a/tests/smbo/lib/Transfer.php b/tests/smbo/lib/Transfer.php index e050518b4..ce7cb7f65 100644 --- a/tests/smbo/lib/Transfer.php +++ b/tests/smbo/lib/Transfer.php @@ -21,7 +21,7 @@ public function init() $this->addField('destination_account_id', ['never_persist' => true]); - $this->addHook('beforeSave', function ($m) { + $this->onHook('beforeSave', function ($m) { // only for new records and when destination_account_id is set if ($m['destination_account_id'] && !$m->id) { @@ -53,7 +53,7 @@ public function init() } }); - $this->addHook('afterSave', function ($m) { + $this->onHook('afterSave', function ($m) { if ($m->other_leg_creation) { $m->other_leg_creation->set('transfer_document_id', $m->id)->save(); }