diff --git a/.github/workflows/bundler.yml b/.github/workflows/bundler.yml index 72486fbf0..107fac9ca 100644 --- a/.github/workflows/bundler.yml +++ b/.github/workflows/bundler.yml @@ -14,32 +14,29 @@ jobs: - run: echo ${{ github.ref }} - name: Update to stable dependencies run: | - jq 'del(.require["atk4/core"]) | del(.require["atk4/dsql"]) | del(.["require-dev"]["atk4/schema"])' < composer.json > tmp && mv tmp composer.json - composer require --no-progress --no-suggest --prefer-dist --optimize-autoloader atk4/core atk4/dsql - composer require --no-progress --no-suggest --prefer-dist --optimize-autoloader --dev atk4/schema - composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader + # replaces X keys with X-release keys + jq '. as $in | reduce (keys_unsorted[] | select(endswith("-release")|not)) as $k ({}; . + {($k) : (($k + "-release") as $kr | $in | if has($kr) then .[$kr] else .[$k] end) } )' < composer.json > tmp && mv tmp composer.json + v=$(echo ${{ github.ref }} | cut -d / -f 4) + echo "::set-env name=version::$v" - uses: teaminkling/autocommit@master with: - commit-message: Setting current dependencies + commit-message: Setting release dependencies - uses: ad-m/github-push-action@master with: branch: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: pull-request - uses: repo-sync/pull-request@v2 + uses: romaninsh/pull-request@master with: - source_branch: "" # If blank, default: triggered branch + source_branch: "release/${{ env.version }}" destination_branch: "master" # If blank, default: master - pr_title: "Releasing ${{ github.ref }} into master" + pr_title: "Releasing ${{ env.version }} into master" pr_body: | - [ ] Review changes (must include stable dependencies) - [ ] Merge this PR into master (will delete ${{ github.ref }}) - [ ] Go to Releases and create TAG from master - - ---------- - Do not merge master into develop pr_reviewer: "romaninsh" pr_assignee: "romaninsh" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 59fb9817d..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@v5.2.0 + - 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..8ea497f6a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -15,6 +15,7 @@ jobs: container: image: atk4/image:${{ matrix.php }} # https://github.com/atk4/image strategy: + fail-fast: false matrix: php: ['7.2', '7.3', 'latest'] services: @@ -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 diff --git a/composer.json b/composer.json index 896633279..9572ef4c1 100644 --- a/composer.json +++ b/composer.json @@ -1,39 +1,69 @@ { - "name": "atk4/data", - "type": "library", - "description": "Agile Data - Database access abstraction framework", - "keywords": ["framework", "orm", "query", "active record", "sql", "builder", "nosql", "mongodb", "mysql", "postgresql"], - "homepage": "https://github.com/atk4/data", - "license": "MIT", - "authors": [ - { - "name": "Romans Malinovskis", - "email": "romans@agiletoolkit.org", - "homepage": "https://nearly.guru/" - } - ], - "require": { - "php": ">=7.2.0", - "ext-intl": "*", + "name": "atk4/data", + "type": "library", + "description": "Agile Data - Database access abstraction framework", + "keywords": [ + "framework", + "orm", + "query", + "active record", + "sql", + "builder", + "nosql", + "mongodb", + "mysql", + "oracle", + "postgresql" + ], + "homepage": "https://github.com/atk4/data", + "license": "MIT", + "authors": [ + { + "name": "Romans Malinovskis", + "email": "romans@agiletoolkit.org", + "homepage": "https://nearly.guru/" + } + ], + "require": { + "php": ">=7.2.0", + "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "atk4/core": "dev-develop", - "atk4/dsql": "dev-develop" - }, - "require-dev": { - "atk4/schema": "dev-develop", - "phpunit/phpunit": "<6", - "phpunit/dbunit": ">=1.2", - "phpunit/phpcov": "*", - "codeclimate/php-test-reporter": "*" - }, - "autoload": { - "psr-4": {"atk4\\data\\":"src/"} - }, - "autoload-dev": { - "psr-4": { - "atk4\\data\\tests\\":"tests/", - "atk4\\data\\tests\\smbo\\":["tests/smbo","tests/smbo/lib"] - } + "atk4/core": "dev-develop", + "atk4/dsql": "dev-develop" + }, + "require-release": { + "php": ">=7.2.0", + "ext-intl": "*", + "atk4/core": "^2.0", + "atk4/dsql": "^2.0" + }, + "require-dev": { + "atk4/schema": "dev-develop", + "phpunit/phpunit": "<6", + "phpunit/dbunit": ">=1.2", + "phpunit/phpcov": "*", + "codeclimate/php-test-reporter": "*" + }, + "require-dev-release": { + "atk4/schema": "^2.0", + "phpunit/phpunit": "<6", + "phpunit/dbunit": ">=1.2", + "phpunit/phpcov": "*", + "codeclimate/php-test-reporter": "*" + }, + "autoload": { + "psr-4": { + "atk4\\data\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "atk4\\data\\tests\\": "tests/", + "atk4\\data\\tests\\smbo\\": [ + "tests/smbo", + "tests/smbo/lib" + ] } + } } diff --git a/docs/advanced.rst b/docs/advanced.rst index 8cac3e7c0..07ffb3ecf 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', .......... ) } @@ -155,7 +155,7 @@ which I want to define like this:: return; } - $this->owner->addField('created_dts', ['type'=>'datetime', 'default'=>date('Y-m-d H:i:s')]); + $this->owner->addField('created_dts', ['type'=>'datetime', 'default'=>new \DateTime()]); $this->owner->hasOne('created_by_user_id', 'User'); if(isset($this->app->user) and $this->app->user->loaded()) { @@ -166,11 +166,11 @@ 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; } - $data['updated_dts'] = date('Y-m-d H:i:s'); + $data['updated_dts'] = new \DateTime(); }); } @@ -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) @@ -488,7 +488,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 +623,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 +712,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 f46dde72f..3e2203153 100644 --- a/docs/model.rst +++ b/docs/model.rst @@ -29,12 +29,14 @@ object you can load/unload individual records (See Single record Operations belo $m->load(8); .... -and even perform operations on multiple records (See Multiple record Operations below):: +and even perform operations on multiple records (See `Persistence Actions` below):: $m = new User($db); $m->addCondition('expired', true); - $m->deleteAll(); + $m->action('delete')->execute(); // performs mass delete, hooks are not executed + + $m->each('delete'); // deletes each record, hooks are executed When data is loaded from associated Persistence, it is automatically converted into a native PHP type (such as DateTime object) through a process called Typecasting. Various @@ -185,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(); @@ -389,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']; } @@ -435,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); } @@ -738,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/Field.php b/src/Field.php index 1eba10e02..cfe2a75e8 100644 --- a/src/Field.php +++ b/src/Field.php @@ -5,6 +5,7 @@ namespace atk4\data; use atk4\core\DIContainerTrait; +use atk4\core\InitializerTrait; use atk4\core\ReadableCaptionTrait; use atk4\core\TrackableTrait; use atk4\dsql\Expression; @@ -18,6 +19,9 @@ class Field implements Expressionable use TrackableTrait; use DIContainerTrait; use ReadableCaptionTrait; + use InitializerTrait { + init as _init; + } // {{{ Properties @@ -245,6 +249,14 @@ public function __construct($defaults = []) } } + /** + * Initialization. + */ + public function init() + { + $this->_init(); + } + /** * Validate and normalize value. * @@ -261,74 +273,21 @@ public function __construct($defaults = []) */ public function normalize($value) { - // SQL fields are allowed to have expressions inside of them. - if ($value instanceof Expression || - $value instanceof Expressionable) { - return $value; - } - // NULL value is always fine if it is allowed - if ($value === null) { - if ($this->required) { - throw new ValidationException([$this->name => 'Must not be null']); - } - - return; - } - - $f = $this; - - // only string type fields can use empty string as legit value, for all - // other field types empty value is the same as no-value, nothing or null - if ($f->type && $f->type != 'string' && $value === '') { + if ($value === null || $value === '') { if ($this->required) { - throw new ValidationException([$this->name => 'Must not be empty']); - } - - return; - } - - // validate scalar values - if (in_array($f->type, ['string', 'text', 'integer', 'money', 'float']) && !is_scalar($value)) { - throw new ValidationException([$this->name => 'Must use scalar value']); - } - - // normalize - // @TODO remove this block in future - it's useless - switch ($f->type) { - case null: // loose comparison, but is OK here - // NOTE - this is not always the same as type=string. Need to review what else it can be and how type=null is used at all - if ($this->required && empty($value)) { - throw new ValidationException([$this->name => 'Must not be empty']); + throw new ValidationException([$this->name => 'Must not be null or empty']); } - break; - case 'string': - throw new Exception(['Use Field\Line for type=string', 'this'=>$this]); - case 'text': - throw new Exception(['Use Field\Text for type=text', 'this'=>$this]); - case 'integer': - throw new Exception(['Use Field\Integer for type=integer', 'this'=>$this]); - case 'float': - throw new Exception(['Use Field\Numeric for type=float', 'this'=>$this]); - case 'money': - throw new Exception(['Use Field\Money for type=money', 'this'=>$this]); - case 'boolean': - throw new Exception(['Use Field\Boolean for type=boolean', 'this'=>$this]); - case 'date': - throw new Exception(['Use Field\Date for type=date', 'this'=>$this]); - case 'datetime': - throw new Exception(['Use Field\DateTime for type=datetime', 'this'=>$this]); - case 'time': - throw new Exception(['Use Field\Time for type=time', 'this'=>$this]); - case 'array': - throw new Exception(['Use Field\Array_ for type=array', 'this'=>$this]); - case 'object': - throw new Exception(['Use Field\Object_ for type=object', 'this'=>$this]); } return $value; } + public static function isExpression($value) + { + return $value instanceof Expression || $value instanceof Expressionable; + } + /** * Return array of seed properties of this Field object. * @@ -512,6 +471,20 @@ public function getCaption(): string /** * Returns typecasting callback if defined. * + * Typecasting can be defined as (in order of precedence) + * + * * affects all typecasting for the field + * $user->addField('dob', ['Date', 'typecast'=>[$encode_fx, $decode_fx]]); + * + * * affects typecasting for specific persistence class + * $user->addField('dob', ['Date', 'persistence'=>['atk4\data\Persistence\SQL'=>['typecast'=>[$encode_fx, $decode_fx]]]]); + * + * * affects typecasting for all persistences + * $user->addField('dob', ['Date', 'persistence'=>['typecast'=>[$encode_fx, $decode_fx]]]); + * + * * default typecasting (if none of above set) will be used for all fields of the class defined in field methods + * typecastSave / typecastLoad based on the $mode + * * @param string $mode - load|save * * @return callable|false @@ -525,7 +498,12 @@ public function getTypecaster($mode) 'load' => 1, ]; - $fx = $this->typecast[$mode] ?? $this->typecast[$map[$mode]] ?? false; + $typecast = $this->getPersistenceSetting('typecast'); + + // default typecaster is method in the field named typecastSave or typecastLoad if such method exists + $default = method_exists($this, 'typecast'.ucfirst($mode)) ? [$this, 'typecast'.ucfirst($mode)] : false; + + $fx = $typecast[$mode] ?? $typecast[$map[$mode]] ?? $default; return is_callable($fx) ? $fx : false; } @@ -551,6 +529,50 @@ public function getSerializer($mode) return is_callable($fx) ? $fx : false; } + /** + * Returns persistence setting defined + * Order of precedence is: field specific, persistence specific, persistence general. + * + * Below examples consider $key = 'typecast' + * Field specific setting is defined in a field property with $key as name + * e.g. $field->typecast = [$encode_fx, $decode_fx] + * + * Persistence specific setting is defined in $field->persistence array + * e.g. $field->persistence = [\atk4\data\Persistence\SQL::class => ['typecast' => [$encode_fx, $decode_fx]]] or + * e.g. $field->persistence = ['SQL' => ['typecast' => [$encode_fx, $decode_fx]]] + * The latter checks only the persistence class name ignoring the namespace. + * Both syntaxes are valid but first one has precedence + * + * Persistence general setting is defined in $field->persistence array + * e.g. $field->persistence = ['typecast' => [$encode_fx, $decode_fx]] + * + * @param string $key + * + * @return array + */ + public function getPersistenceSetting($key) + { + // persistence specific typecast + $specific = null; + if ($persistence = $this->hasPersistence()) { + $classFull = get_class($persistence); + $classBare = implode('', array_slice(explode('\\', $classFull), -1)); + + foreach ([$classFull, $classBare] as $class) { + $specific = $this->persistence[$class][$key] ?? $specific; + } + } + + // get the setting definition to be applied + // field specific or persistence specific or persistence general or none + return $this->{$key} ?? $specific ?? $this->persistence[$key] ?? []; + } + + public function hasPersistence() + { + return $this->owner ? $this->owner->persistence : false; + } + // }}} /** diff --git a/src/Field/Array_.php b/src/Field/Array_.php index 310a01337..79d5baf67 100644 --- a/src/Field/Array_.php +++ b/src/Field/Array_.php @@ -34,8 +34,10 @@ public function normalize($value) return; } - if (is_string($value) && $this->owner && $this->owner->persistence) { - $value = $this->owner->persistence->jsonDecode($this, $value, true); + if (is_string($value)) { + if ($persistence = $this->hasPersistence()) { + $value = $persistence->jsonDecode($this, $value, true); + } } if (!is_array($value)) { @@ -56,8 +58,8 @@ public function toString($value = null): ?string { $v = ($value === null ? $this->get() : $this->normalize($value)); - if ($this->owner && $this->owner->persistence) { - $v = $this->owner->persistence->jsonEncode($this, $v); + if ($persistence = $this->hasPersistence()) { + $v = $persistence->jsonEncode($this, $v); } else { $v = json_encode($v); } diff --git a/src/Field/Boolean.php b/src/Field/Boolean.php index 9b06f3c4d..e8e9ace10 100644 --- a/src/Field/Boolean.php +++ b/src/Field/Boolean.php @@ -4,7 +4,6 @@ namespace atk4\data\Field; -use atk4\core\InitializerTrait; use atk4\data\ValidationException; /** @@ -12,10 +11,6 @@ */ class Boolean extends \atk4\data\Field { - use InitializerTrait { - init as _init; - } - /** @var string Field type for backward compatibility. */ public $type = 'boolean'; @@ -52,7 +47,7 @@ class Boolean extends \atk4\data\Field */ public function init() { - $this->_init(); + parent::init(); // Backwards compatibility if ($this->enum) { @@ -73,32 +68,26 @@ public function init() */ public function normalize($value) { - if ($value === null || $value === '') { - if ($this->required) { - throw new ValidationException([$this->name => 'Must not be null or empty']); - } - + if (is_null($value) || $value === '') { return; } + if (is_bool($value)) { + return $value; + } if ($value === $this->valueTrue) { - $value = true; - } elseif ($value === $this->valueFalse) { - $value = false; - } elseif (is_numeric($value)) { - $value = (bool) $value; + return true; } - if (!is_bool($value)) { - throw new ValidationException([$this->name => 'Must be a boolean value']); + if ($value === $this->valueFalse) { + return false; } - // if value required, then only valueTrue is allowed - if ($this->required && $value !== true) { - throw new ValidationException([$this->name => 'Must be selected']); + if (is_numeric($value)) { + return (bool) $value; } - return $value; + throw new ValidationException([$this->name => 'Must be a boolean value']); } /** diff --git a/src/Field/Callback.php b/src/Field/Callback.php index 9de07f199..010a2e74e 100644 --- a/src/Field/Callback.php +++ b/src/Field/Callback.php @@ -4,17 +4,11 @@ namespace atk4\data\Field; -use atk4\core\InitializerTrait; - /** * Evaluate php expression after load. */ class Callback extends \atk4\data\Field { - use InitializerTrait { - init as _init; - } - /** * Method to execute for evaluation. * @@ -55,10 +49,12 @@ class Callback extends \atk4\data\Field */ public function init() { - $this->_init(); + parent::init(); + + $this->ui['table']['sortable'] = false; - $this->owner->addHook('afterLoad', function ($m) { - $m->data[$this->short_name] = call_user_func($this->fx ?: $this->expr, $m); + $this->owner->onHook('afterLoad', function ($model) { + $model->data[$this->short_name] = call_user_func($this->fx ?: $this->expr, $model); }); } } diff --git a/src/Field/Date.php b/src/Field/Date.php index 941b833b9..18e5bd6d7 100644 --- a/src/Field/Date.php +++ b/src/Field/Date.php @@ -27,7 +27,7 @@ public function normalize($value) if ($value !== null) { // remove time portion from date type value - $value->setTime(0, 0, 0); + $value = (clone $value)->setTime(0, 0, 0); } return $value; diff --git a/src/Field/DateTime.php b/src/Field/DateTime.php index c7c5f8e79..b0fc515b6 100644 --- a/src/Field/DateTime.php +++ b/src/Field/DateTime.php @@ -77,18 +77,22 @@ public function normalize($value) } // we allow http://php.net/manual/en/datetime.formats.relative.php - $class = $this->dateTimeClass ?? self::class; + $class = $this->dateTimeClass ?? \DateTime::class; if (is_numeric($value)) { $value = new $class('@'.$value); } elseif (is_string($value)) { $value = new $class($value); } elseif (!$value instanceof $class) { - if (is_object($value)) { - throw new ValidationException(['must be a '.$this->type, 'class' => $class, 'value class' => get_class($value)]); + if ($value instanceof \DateTimeInterface) { + $value = new $class($value->format('Y-m-d H:i:s.u'), $value->getTimezone()); + } else { + if (is_object($value)) { + throw new ValidationException(['must be a '.$this->type, 'class' => $class, 'value class' => get_class($value)]); + } + + throw new ValidationException(['must be a '.$this->type, 'class' => $class, 'value type' => gettype($value)]); } - - throw new ValidationException(['must be a '.$this->type, 'class' => $class, 'value type' => gettype($value)]); } return $value; @@ -105,6 +109,15 @@ public function toString($value = null): ?string { $v = ($value === null ? $this->get() : $this->normalize($value)); - return $v ? $v->format('c') : $v; // ISO 8601 format 2004-02-12T15:19:21+00:00 + if ($v) { + $dateFormat = 'Y-m-d'; + $timeFormat = 'H:i:s'.($v->format('u') > 0 ? '.u' : ''); + + $format = $dateFormat.'\T'.$timeFormat.'P'; // ISO 8601 format 2004-02-12T15:19:21+00:00 + + $v = $v->format($format); + } + + return $v; } } diff --git a/src/Field/Line.php b/src/Field/Line.php index b81d4cdb1..341931f59 100644 --- a/src/Field/Line.php +++ b/src/Field/Line.php @@ -37,11 +37,7 @@ class Line extends Text */ public function normalize($value) { - $value = parent::normalize($value); - // remove all line-ends - $value = trim(str_replace(["\r", "\n"], '', $value)); - - return $value; + return trim(str_replace(["\r", "\n"], '', parent::normalize($value))); } } diff --git a/src/Field/Numeric.php b/src/Field/Numeric.php index 6169ad249..34808526d 100644 --- a/src/Field/Numeric.php +++ b/src/Field/Numeric.php @@ -102,26 +102,26 @@ public function normalize($value) /** * Round up to the nearest number. * - * @param float $n Number - * @param int $p Precision + * @param float $number Number + * @param int $precision Precision * * @return float */ - protected function roundUp(float $n, int $p): float + protected function roundUp(float $number, int $precision): float { - return $p ? ceil($n / $p) * $p : ceil($n); + return $precision ? ceil($number / $precision) * $precision : ceil($number); } /** * Round down to the nearest number. * - * @param float $n Number - * @param int $p Precision + * @param float $number Number + * @param int $precision Precision * * @return float */ - protected function roundDown(float $n, int $p): float + protected function roundDown(float $number, int $precision): float { - return $p ? floor($n / $p) * $p : floor($n); + return $precision ? floor($number / $precision) * $precision : floor($number); } } diff --git a/src/Field/Object_.php b/src/Field/Object_.php index 0a045d5a0..11213fbb3 100644 --- a/src/Field/Object_.php +++ b/src/Field/Object_.php @@ -28,14 +28,16 @@ public function normalize($value) { if ($value === null || $value === '') { if ($this->required) { - throw new ValidationException([$this->name => 'Must not be null']); + throw new ValidationException([$this->name => 'Must not be null or empty']); } return; } - if (is_string($value) && $this->owner && $this->owner->persistence) { - $value = $this->owner->persistence->jsonDecode($this, $value, false); + if (is_string($value)) { + if ($persistence = $this->hasPersistence()) { + $value = $persistence->jsonDecode($this, $value, false); + } } if (!is_object($value)) { @@ -56,8 +58,8 @@ public function toString($value = null): ?string { $v = ($value === null ? $this->get() : $this->normalize($value)); - if ($this->owner && $this->owner->persistence) { - $v = $this->owner->persistence->jsonEncode($this, $v); + if ($persistence = $this->hasPersistence()) { + $v = $persistence->jsonEncode($this, $v); } else { $v = json_encode($v); } diff --git a/src/Field/Time.php b/src/Field/Time.php index 0e5feaa1e..45d5ee52f 100644 --- a/src/Field/Time.php +++ b/src/Field/Time.php @@ -30,7 +30,7 @@ public function normalize($value) if ($value !== null) { // remove date portion from date type value // need 1970 in place of 0 - DB - $value->setDate(1970, 1, 1); + $value = (clone $value)->setDate(1970, 1, 1); } return $value; @@ -47,6 +47,6 @@ public function toString($value = null): ?string { $v = ($value === null ? $this->get() : $this->normalize($value)); - return $v ? $v->format('H:i:s') : $v; + return $v ? $v->format('H:i:s'.($v->format('u') > 0 ? '.u' : '')) : $v; } } diff --git a/src/Field_SQL_Expression.php b/src/Field_SQL_Expression.php index e5e9b12de..d8a1789aa 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); } } @@ -97,8 +97,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..4971a2910 100644 --- a/src/Join.php +++ b/src/Join.php @@ -160,7 +160,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 +181,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 +204,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..a7f7ab6c8 100644 --- a/src/Join/SQL.php +++ b/src/Join/SQL.php @@ -69,17 +69,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 +99,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 +131,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 9ae2ae155..4f24b1e1f 100644 --- a/src/Model.php +++ b/src/Model.php @@ -719,15 +719,16 @@ public function isDirty($fields = []) } /** - * @param string|null $filter + * @param string|array|null $filter * * @return array */ - public function getFields(string $filter = null) + public function getFields($filter = null) { if (!$filter) { return $this->fields; } + $filter = is_string($filter) ? explode(',', $filter) : $filter; return array_filter($this->fields, function (Field $field, $name) use ($filter) { @@ -735,15 +736,20 @@ public function getFields(string $filter = null) if ($this->only_fields && !in_array($name, $this->only_fields)) { return false; } - - switch ($filter) { - case 'system': return $field->system; - case 'not system': return !$field->system; - case 'editable': return $field->isEditable(); - case 'visible': return $field->isVisible(); - default: - throw new Exception(['Filter is not supported', 'filter'=>$filter]); + foreach ($filter as $f) { + if ( + ($f == 'system' && $field->system) + || ($f == 'not system' && !$field->system) + || ($f == 'editable' && $field->isEditable()) + || ($f == 'visible' && $field->isVisible()) + ) { + return true; + } elseif (!in_array($f, ['system', 'not system', 'editable', 'visible'])) { + throw new Exception(['Filter is not supported', 'filter'=>$f]); + } } + + return false; }, ARRAY_FILTER_USE_BOTH); } @@ -789,7 +795,7 @@ public function set($field, $value = null) && $this->hook('normalize', [$f, $value]) !== false && $this->strict_types ) { - $value = $f->normalize($value); + $value = $f->isExpression($value) ? $value : $f->normalize($value); } } catch (Exception $e) { $e->addMoreInfo('field', $field); @@ -940,6 +946,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]) @@ -2084,7 +2104,7 @@ public function getIterator() // 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); // }) diff --git a/src/Persistence/SQL.php b/src/Persistence/SQL.php index 610c90d46..97367694e 100644 --- a/src/Persistence/SQL.php +++ b/src/Persistence/SQL.php @@ -217,8 +217,7 @@ function ($matches) use (&$args, $model) { /** * Creates new Query object with current_timestamp(precision) expression. * - * @param Model $m - * @param int $precision + * @param int $precision * * @return Query */ @@ -445,8 +444,8 @@ public function _typecastSaveField(Field $field, $value) $dt_class = $field->dateTimeClass ?? 'DateTime'; $tz_class = $field->dateTimeZoneClass ?? 'DateTimeZone'; - if ($v instanceof $dt_class) { - $format = ['date' => 'Y-m-d', 'datetime' => 'Y-m-d H:i:s', 'time' => 'H:i:s']; + 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']; $format = $field->persistence['format'] ?? $format[$field->type]; // datetime only - set to persisting timezone @@ -486,76 +485,85 @@ public function _typecastLoadField(Field $field, $value) $v = is_object($value) ? clone $value : $value; switch ($field->type) { - case 'string': - case 'text': - // do nothing - it's ok as it is - break; - case 'integer': - $v = (int) $v; - break; - case 'float': - $v = (float) $v; - break; - case 'money': - $v = round($v, 4); - break; - case 'boolean': - if (isset($field->enum) && is_array($field->enum)) { - if (isset($field->enum[0]) && $v == $field->enum[0]) { - $v = false; - } elseif (isset($field->enum[1]) && $v == $field->enum[1]) { - $v = true; - } else { + case 'string': + case 'text': + // do nothing - it's ok as it is + break; + case 'integer': + $v = (int) $v; + break; + case 'float': + $v = (float) $v; + break; + case 'money': + $v = round($v, 4); + break; + case 'boolean': + if (isset($field->enum) && is_array($field->enum)) { + if (isset($field->enum[0]) && $v == $field->enum[0]) { + $v = false; + } elseif (isset($field->enum[1]) && $v == $field->enum[1]) { + $v = true; + } else { + $v = null; + } + } elseif ($v === '') { $v = null; + } else { + $v = (bool) $v; } - } elseif ($v === '') { - $v = null; - } else { - $v = (bool) $v; - } - break; - case 'date': - case 'datetime': - case 'time': - $dt_class = isset($field->dateTimeClass) ? $field->dateTimeClass : 'DateTime'; - $tz_class = isset($field->dateTimeZoneClass) ? $field->dateTimeZoneClass : 'DateTimeZone'; - - if (is_numeric($v)) { - $v = new $dt_class('@'.$v); - } elseif (is_string($v)) { - // ! symbol in date format is essential here to remove time part of DateTime - don't remove, this is not a bug - $format = ['date' => '+!Y-m-d', 'datetime' => '+!Y-m-d H:i:s', 'time' => '+!H:i:s']; - $format = $field->persistence['format'] ?? $format[$field->type]; + break; + case 'date': + case 'datetime': + case 'time': + $dt_class = isset($field->dateTimeClass) ? $field->dateTimeClass : 'DateTime'; + $tz_class = isset($field->dateTimeZoneClass) ? $field->dateTimeZoneClass : 'DateTimeZone'; + + if (is_numeric($v)) { + $v = new $dt_class('@'.$v); + } elseif (is_string($v)) { + if ($field->persistence['format'] ?? null) { + $format = $field->persistence['format']; + } else { + // ! symbol in date format is essential here to remove time part of DateTime - don't remove, this is not a bug + $formatMap = ['date' => '+!Y-m-d', 'datetime' => '+!Y-m-d H:i:s', 'time' => '+!H:i:s']; + + $format = $formatMap[$field->type]; + + if (strpos($v, '.') !== false) { // time possibly with microseconds, otherwise invalid format + $format = preg_replace('~(?<=H:i:s)(?![. ]*u)~', '.u', $format); + } + } - // datetime only - set from persisting timezone - if ($field->type == 'datetime' && isset($field->persistence['timezone'])) { - $v = $dt_class::createFromFormat($format, $v, new $tz_class($field->persistence['timezone'])); - if ($v === false) { - throw new Exception(['Incorrectly formatted datetime', 'format' => $format, 'value' => $value, 'field' => $field]); + // datetime only - set from persisting timezone + if ($field->type == 'datetime' && isset($field->persistence['timezone'])) { + $v = $dt_class::createFromFormat($format, $v, new $tz_class($field->persistence['timezone'])); + if ($v !== false) { + $v->setTimezone(new $tz_class(date_default_timezone_get())); + } + } else { + $v = $dt_class::createFromFormat($format, $v); } - $v->setTimeZone(new $tz_class(date_default_timezone_get())); - } else { - $v = $dt_class::createFromFormat($format, $v); + if ($v === false) { throw new Exception(['Incorrectly formatted date/time', 'format' => $format, 'value' => $value, 'field' => $field]); } - } - // need to cast here because DateTime::createFromFormat returns DateTime object not $dt_class - // this is what Carbon::instance(DateTime $dt) method does for example - if ($dt_class != 'DateTime') { - $v = new $dt_class($v->format('Y-m-d H:i:s.u'), $v->getTimeZone()); + // need to cast here because DateTime::createFromFormat returns DateTime object not $dt_class + // this is what Carbon::instance(DateTime $dt) method does for example + if ($dt_class != 'DateTime') { + $v = new $dt_class($v->format('Y-m-d H:i:s.u'), $v->getTimeZone()); + } } - } - break; - case 'array': - // don't decode if we already use some kind of serialization - $v = $field->serialize ? $v : $this->jsonDecode($field, $v, true); - break; - case 'object': - // don't decode if we already use some kind of serialization - $v = $field->serialize ? $v : $this->jsonDecode($field, $v, false); - break; + break; + case 'array': + // don't decode if we already use some kind of serialization + $v = $field->serialize ? $v : $this->jsonDecode($field, $v, true); + break; + case 'object': + // don't decode if we already use some kind of serialization + $v = $field->serialize ? $v : $this->jsonDecode($field, $v, false); + break; } return $v; @@ -987,15 +995,11 @@ public function delete(Model $model, $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/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/HasOne.php b/src/Reference/HasOne.php index c815c20bb..3794004c3 100644 --- a/src/Reference/HasOne.php +++ b/src/Reference/HasOne.php @@ -193,7 +193,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; }); @@ -203,19 +203,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 11c12ef5a..bf8b79d7c 100644 --- a/src/Reference/HasOne_SQL.php +++ b/src/Reference/HasOne_SQL.php @@ -52,14 +52,15 @@ 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([ + $e = $this->owner->addExpression($field, array_merge( + [ - // get expression - 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'); - }, ], + // get expression + 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'); + }, ], // get field seed properties from referenced model $this->refLink()->getField($their_field)->getSeed(), @@ -72,7 +73,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)) { @@ -82,7 +83,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; } @@ -233,7 +234,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); @@ -253,7 +254,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)) { @@ -262,7 +263,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/Util/DeepCopy.php b/src/Util/DeepCopy.php index 4ca3a5a11..3b00d2294 100644 --- a/src/Util/DeepCopy.php +++ b/src/Util/DeepCopy.php @@ -177,12 +177,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/ContainsManyTest.php b/tests/ContainsManyTest.php index 797525d39..0330148d1 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); @@ -291,11 +291,13 @@ public function testNestedContainsMany() ], ], // json decode and also decode nested discounts array to be able to compare them - array_map(function ($v) { + array_map( + function ($v) { $v['discounts'] = json_decode($v['discounts'], true); return $v; - }, json_decode($exp_lines, true) + }, + json_decode($exp_lines, true) ) ); /* diff --git a/tests/ContainsOneTest.php b/tests/ContainsOneTest.php index 675266d78..61b2ab3bc 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); 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 ff6305eb1..1d0a1cd47 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']); @@ -843,6 +843,8 @@ public function testGetFields() $this->assertEquals(['editable', 'visible', 'not_editable'], array_keys($model->getFields('not system'))); $this->assertEquals(['editable', 'editable_system', 'visible'], array_keys($model->getFields('editable'))); $this->assertEquals(['editable', 'visible', 'visible_system', 'not_editable'], array_keys($model->getFields('visible'))); + $this->assertEquals(['editable', 'editable_system', 'visible', 'visible_system', 'not_editable'], array_keys($model->getFields(['editable', 'visible']))); + $this->assertEquals(['editable', 'editable_system', 'visible', 'visible_system', 'not_editable'], array_keys($model->getFields('visible,editable'))); $model->onlyFields(['system', 'visible', 'not_editable']); @@ -867,14 +869,25 @@ public function testDateTimeFieldsToString() $this->assertEquals('', $model->getField('time')->toString()); $this->assertEquals('', $model->getField('datetime')->toString()); - $current_date = new \DateTime(); - $model->set('date', $current_date); - $model->set('time', $current_date); - $model->set('datetime', $current_date); - - $this->assertEquals($current_date->format('Y-m-d'), $model->getField('date')->toString()); - $this->assertEquals($current_date->format('H:i:s'), $model->getField('time')->toString()); - $this->assertEquals($current_date->format('c'), $model->getField('datetime')->toString()); + // datetime without microseconds + $dt = new \DateTime('2020-01-21 21:09:42'); + $model->set('date', $dt); + $model->set('time', $dt); + $model->set('datetime', $dt); + + $this->assertEquals($dt->format('Y-m-d'), $model->getField('date')->toString()); + $this->assertEquals($dt->format('H:i:s'), $model->getField('time')->toString()); + $this->assertEquals($dt->format('c'), $model->getField('datetime')->toString()); + + // datetime with microseconds + $dt = new \DateTime('2020-01-21 21:09:42.895623'); + $model->set('date', $dt); + $model->set('time', $dt); + $model->set('datetime', $dt); + + $this->assertEquals($dt->format('Y-m-d'), $model->getField('date')->toString()); + $this->assertEquals($dt->format('H:i:s.u'), $model->getField('time')->toString()); + $this->assertEquals($dt->format('Y-m-d\TH:i:s.uP'), $model->getField('datetime')->toString()); } public function testFieldSeed() 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/RandomTest.php b/tests/RandomTest.php index e33027cc2..5240aadb6 100644 --- a/tests/RandomTest.php +++ b/tests/RandomTest.php @@ -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); }); @@ -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,27 @@ 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 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..4356ab7c4 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); @@ -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() 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 9f91c4df1..aa43801ad 100644 --- a/tests/TypecastingTest.php +++ b/tests/TypecastingTest.php @@ -17,7 +17,7 @@ class MyTime extends \DateTime { public function __toString() { - return $this->format('H:i:s'); + return $this->format('H:i:s.u'); } } @@ -25,7 +25,7 @@ class MyDateTime extends \DateTime { public function __toString() { - return date('Y-m-d H:i:s', $this->format('U')); + return $this->format('Y-m-d H:i:s.u'); } } @@ -41,8 +41,8 @@ public function testType() [ 'string' => 'foo', 'date' => '2013-02-20', - 'datetime' => '2013-02-20 20:00:12', - 'time' => '12:00:50', + 'datetime' => '2013-02-20 20:00:12.000000', + 'time' => '12:00:50.000000', 'boolean' => 1, 'integer' => '2940', 'money' => '8.20', @@ -87,8 +87,8 @@ public function testType() 'id' => '1', 'string' => 'foo', 'date' => '2013-02-20', - 'datetime' => '2013-02-20 20:00:12', - 'time' => '12:00:50', + 'datetime' => '2013-02-20 20:00:12.000000', + 'time' => '12:00:50.000000', 'boolean' => 1, 'integer' => 2940, 'money' => 8.20, @@ -99,8 +99,8 @@ public function testType() 'id' => '2', 'string' => 'foo', 'date' => '2013-02-20', - 'datetime' => '2013-02-20 20:00:12', - 'time' => '12:00:50', + 'datetime' => '2013-02-20 20:00:12.000000', + 'time' => '12:00:50.000000', 'boolean' => '1', 'integer' => '2940', 'money' => '8.2', @@ -248,8 +248,8 @@ public function testTypeCustom1() 'types' => [ [ 'date' => '2013-02-20', - 'datetime' => '2013-02-20 20:00:12', - 'time' => '12:00:50', + 'datetime' => '2013-02-20 20:00:12.235689', + 'time' => '12:00:50.235689', 'b1' => 'Y', 'b2' => 'N', 'integer' => '2940', @@ -285,9 +285,9 @@ public function testTypeCustom1() $this->assertSame('hello world', $m['rot13']); $this->assertSame(1, (int) $m->id); $this->assertSame(1, (int) $m['id']); - $this->assertEquals('2013-02-21 05:00:12', (string) $m['datetime']); + $this->assertEquals('2013-02-21 05:00:12.235689', (string) $m['datetime']); $this->assertEquals('2013-02-20', (string) $m['date']); - $this->assertEquals('12:00:50', (string) $m['time']); + $this->assertEquals('12:00:50.235689', (string) $m['time']); $this->assertEquals(true, $m['b1']); $this->assertEquals(false, $m['b2']); @@ -299,8 +299,8 @@ public function testTypeCustom1() 2 => [ 'id' => '2', 'date' => '2013-02-20', - 'datetime' => '2013-02-20 20:00:12', - 'time' => '12:00:50', + 'datetime' => '2013-02-20 20:00:12.235689', + 'time' => '12:00:50.235689', 'b1' => 'Y', 'b2' => 'N', 'integer' => '2940', @@ -312,6 +312,46 @@ public function testTypeCustom1() $this->assertEquals($a, $this->getDB()); } + public function testFieldPersistenceSetting() + { + $rot = function ($v) { + return str_rot13($v); + }; + + $typecast = [$rot, $rot]; + + $a = [ + 'various' => [ + [ + 'raw' => 'raw', + 'field_specific' => $rot('raw'), + 'persistence_specific1' => $rot('raw'), + 'persistence_specific2' => $rot('raw'), + 'persistence_general' => $rot('raw'), + ], + ], ]; + + $this->setDB($a); + + $db = new Persistence\SQL($this->db->connection); + + $model = new Model($db, ['table' => 'various']); + + $model->addField('raw'); + $model->addField('field_specific', compact('typecast')); + $model->addField('persistence_specific1', ['persistence' => [Persistence\SQL::class => compact('typecast')]]); + $model->addField('persistence_specific2', ['persistence' => ['SQL' => compact('typecast')]]); + $model->addField('persistence_general', ['persistence' => compact('typecast')]); + + $model->load(1); + + $this->assertEquals('raw', $model['raw']); + $this->assertEquals('raw', $model['field_specific']); + $this->assertEquals('raw', $model['persistence_specific1']); + $this->assertEquals('raw', $model['persistence_specific2']); + $this->assertEquals('raw', $model['persistence_general']); + } + public function testTryLoad() { $a = [ @@ -414,9 +454,9 @@ public function testTypecastTimezone() date_default_timezone_set('UTC'); $s = new \DateTime('Monday, 15-Aug-05 22:52:01 UTC'); - $this->assertEquals('2005-08-16 00:52:01', $db->typecastSaveField($dt, $s)); + $this->assertEquals('2005-08-16 00:52:01.000000', $db->typecastSaveField($dt, $s)); $this->assertEquals('2005-08-15', $db->typecastSaveField($d, $s)); - $this->assertEquals('22:52:01', $db->typecastSaveField($t, $s)); + $this->assertEquals('22:52:01.000000', $db->typecastSaveField($t, $s)); $this->assertEquals(new \DateTime('Monday, 15-Aug-05 22:52:01 UTC'), $db->typecastLoadField($dt, '2005-08-16 00:52:01')); $this->assertEquals(new \DateTime('Monday, 15-Aug-05'), $db->typecastLoadField($d, '2005-08-15')); $this->assertEquals(new \DateTime('1970-01-01 22:52:01'), $db->typecastLoadField($t, '22:52:01')); @@ -424,9 +464,9 @@ public function testTypecastTimezone() date_default_timezone_set('Asia/Tokyo'); $s = new \DateTime('Monday, 15-Aug-05 22:52:01 UTC'); - $this->assertEquals('2005-08-16 00:52:01', $db->typecastSaveField($dt, $s)); + $this->assertEquals('2005-08-16 00:52:01.000000', $db->typecastSaveField($dt, $s)); $this->assertEquals('2005-08-15', $db->typecastSaveField($d, $s)); - $this->assertEquals('22:52:01', $db->typecastSaveField($t, $s)); + $this->assertEquals('22:52:01.000000', $db->typecastSaveField($t, $s)); $this->assertEquals(new \DateTime('Monday, 15-Aug-05 22:52:01 UTC'), $db->typecastLoadField($dt, '2005-08-16 00:52:01')); $this->assertEquals(new \DateTime('Monday, 15-Aug-05'), $db->typecastLoadField($d, '2005-08-15')); $this->assertEquals(new \DateTime('1970-01-01 22:52:01'), $db->typecastLoadField($t, '22:52:01')); @@ -434,9 +474,9 @@ public function testTypecastTimezone() date_default_timezone_set('America/Los_Angeles'); $s = new \DateTime('Monday, 15-Aug-05 22:52:01'); // uses servers default timezone - $this->assertEquals('2005-08-16 07:52:01', $db->typecastSaveField($dt, $s)); + $this->assertEquals('2005-08-16 07:52:01.000000', $db->typecastSaveField($dt, $s)); $this->assertEquals('2005-08-15', $db->typecastSaveField($d, $s)); - $this->assertEquals('22:52:01', $db->typecastSaveField($t, $s)); + $this->assertEquals('22:52:01.000000', $db->typecastSaveField($t, $s)); $this->assertEquals(new \DateTime('Monday, 15-Aug-05 22:52:01 America/Los_Angeles'), $db->typecastLoadField($dt, '2005-08-16 07:52:01')); $this->assertEquals(new \DateTime('Monday, 15-Aug-05'), $db->typecastLoadField($d, '2005-08-15')); $this->assertEquals(new \DateTime('1970-01-01 22:52:01'), $db->typecastLoadField($t, '22:52:01')); 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/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(); }