Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ['8.0', '8.1', '8.2', '8.3']

name: PHP ${{ matrix.php-version }}

steps:
- uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl, pdo_sqlite
coverage: none

- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Run test suite
run: vendor/bin/phpunit

code-quality:
runs-on: ubuntu-latest
name: Code Quality

steps:
- uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: mbstring, intl
coverage: none
tools: cs2pr

- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Run PHPStan
run: vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr
continue-on-error: true

- name: Run PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff --verbose
continue-on-error: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/vendor
composer.phar
composer.lock
.php_cs.cache
.php-cs-fixer.cache
.phpunit.result.cache
.DS_Store
Thumbs.db
15 changes: 15 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
$header = 'This file is part of riesenia/persist-related-data package.

Licensed under the MIT License
(c) RIESENIA.com';

$config = new Rshop\CS\Config\Rshop($header);

$config->setStrict()
->setRule('general_phpdoc_annotation_remove', ['annotations' => ['author']])
->getFinder()
->in(__DIR__)
->exclude('vendor');

return $config;
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Total Downloads](https://img.shields.io/packagist/dt/riesenia/persist-related-data.svg?style=flat-square)](https://packagist.org/packages/riesenia/persist-related-data)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)

This plugin is for CakePHP 3.x and contains behavior that handles saving selected fields
This plugin is for CakePHP 4.x and contains behavior that handles saving selected fields
of related data (redundantly).

## Installation
Expand Down Expand Up @@ -40,7 +40,7 @@ will be filled automatically.
```php
class InvoicesTable extends Table
{
public function initialize(array $config)
public function initialize(array $config): void
{
parent::initialize($config);

Expand All @@ -60,3 +60,25 @@ class InvoicesTable extends Table
}
}
```

### Changeable fields

By default, all fields are automatically overwritten with related data when the foreign key changes.
If you want to allow manual edits to persisted fields while still populating them automatically when
empty, use the `changeable` configuration option:

```php
$this->addBehavior('PersistRelatedData.PersistRelatedData', [
'fields' => [
'contact_name' => 'Contacts.name',
'contact_address' => 'Contacts.address'
],
'changeable' => [
'contact_name' // this field can be manually edited
]
]);
```

With this configuration:
- **contact_name** will be populated from related data only if it's empty
- **contact_address** will always be overwritten with related data when the foreign key changes
10 changes: 7 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
}
],
"require": {
"cakephp/orm": "^3.5"
"php": ">=8.0",
"cakephp/orm": "^4.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.14|^6.0",
"cakephp/cakephp": "^3.5"
"cakephp/cakephp": "^4.0",
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^1.0",
"phpunit/phpunit": "^8.5|^9.3",
"rshop/php-cs-fixer-config": "^3.0"
},
"autoload": {
"psr-4": {
Expand Down
6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
level: max
paths:
- src
bootstrapFiles:
- tests/bootstrap.php
28 changes: 8 additions & 20 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
colors="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php"
>
<php>
Expand All @@ -19,25 +18,14 @@
</testsuites>

<!-- Setup a listener for fixtures -->
<listeners>
<listener
class="\Cake\TestSuite\Fixture\FixtureInjector"
file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
<arguments>
<object class="\Cake\TestSuite\Fixture\FixtureManager" />
</arguments>
</listener>
</listeners>
<extensions>
<extension class="\Cake\TestSuite\Fixture\PHPUnitExtension"/>
</extensions>

<!-- Prevent coverage reports from looking in tests and vendors -->
<filter>
<blacklist>
<directory suffix=".php">./vendor/</directory>
<directory suffix=".ctp">./vendor/</directory>

<directory suffix=".php">./tests/</directory>
<directory suffix=".ctp">./tests/</directory>
</blacklist>
</filter>
<coverage>
<include>
<directory suffix=".php">./src/</directory>
</include>
</coverage>

</phpunit>
53 changes: 37 additions & 16 deletions src/Model/Behavior/PersistRelatedDataBehavior.php
Original file line number Diff line number Diff line change
@@ -1,53 +1,74 @@
<?php
/**
* This file is part of riesenia/persist-related-data package.
*
* Licensed under the MIT License
* (c) RIESENIA.com
*/
declare(strict_types=1);

namespace PersistRelatedData\Model\Behavior;

use Cake\Core\Exception\Exception;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;

/**
* Behavior for persisting selected fields from related table
* Behavior for persisting selected fields from related table.
*
* Set fields option as [field => RelatedTable.related_field]
*/
class PersistRelatedDataBehavior extends Behavior
{
/** @var array */
/**
* @var array<string,mixed>
*/
protected $_defaultConfig = [
'fields' => [],
'changeable' => []
];

/**
* {@inheritDoc}
* @param \Cake\Event\Event<\Cake\ORM\Table> $event
* @param \ArrayObject<string,mixed> $options
*/
public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $options)
public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $options): void
{
$relatedEntities = [];

foreach ($this->getConfig('fields') as $field => $mapped) {
list($mappedTable, $mappedField) = explode('.', $mapped);
$fields = $this->getConfig('fields');

if (!\is_array($fields)) {
return;
}

foreach ($fields as $field => $mapped) {
if (!\is_string($field) || !\is_string($mapped)) {
continue;
}

list($mappedTable, $mappedField) = \explode('.', $mapped);

if (!isset($this->_table->{$mappedTable}) || $this->_table->{$mappedTable}->isOwningSide($this->_table)) {
throw new Exception(sprintf('Incorrect definition of related data to persist for %s', $mapped));
throw new \RuntimeException(\sprintf('Incorrect definition of related data to persist for %s', $mapped));
}

$foreignKeys = $entity->extract((array)$this->_table->{$mappedTable}->getForeignKey());
$dirtyForeignKeys = $entity->extract((array)$this->_table->{$mappedTable}->getForeignKey(), true);
$foreignKeys = $entity->extract((array) $this->_table->{$mappedTable}->getForeignKey());
$dirtyForeignKeys = $entity->extract((array) $this->_table->{$mappedTable}->getForeignKey(), true);

if (!empty($dirtyForeignKeys)) {
// get related entity
if (empty($relatedEntities[$mappedTable])) {
$relatedEntities[$mappedTable] = is_null(array_values($foreignKeys)[0]) ? null : $this->_table->{$mappedTable}->get($foreignKeys);
$relatedEntities[$mappedTable] = \is_null(\array_values($foreignKeys)[0]) ? null : $this->_table->{$mappedTable}->get($foreignKeys);
}

// set field value
if (!is_null($relatedEntities[$mappedTable])) {
if (in_array($field, $this->getConfig('changeable'))) {
$entity->set($field, $entity->get($field) ? : $relatedEntities[$mappedTable]->get($mappedField));
}
else {
if (!\is_null($relatedEntities[$mappedTable])) {
$changeable = $this->getConfig('changeable');

if (\is_array($changeable) && \in_array($field, $changeable, true)) {
$entity->set($field, $entity->get($field) ?: $relatedEntities[$mappedTable]->get($mappedField));
} else {
$entity->set($field, $relatedEntities[$mappedTable]->get($mappedField));
}
}
Expand Down
14 changes: 11 additions & 3 deletions tests/Fixture/ContactsFixture.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<?php
/**
* This file is part of riesenia/persist-related-data package.
*
* Licensed under the MIT License
* (c) RIESENIA.com
*/
declare(strict_types=1);

namespace PersistRelatedData\Test\Fixture;

use Cake\ORM\Table;
use Cake\TestSuite\Fixture\TestFixture;

class ContactsTable extends Table
{
public function initialize(array $config)
public function initialize(array $config): void
{
parent::initialize($config);

Expand All @@ -19,8 +27,8 @@ class ContactsFixture extends TestFixture
{
public $fields = [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string', 'default' => null, 'null' => true],
'address' => ['type' => 'string', 'default' => null, 'null' => true],
'name' => ['type' => 'string', 'length' => 255, 'null' => true],
'address' => ['type' => 'string', 'length' => 255, 'null' => true],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']]
]
Expand Down
18 changes: 13 additions & 5 deletions tests/Fixture/InvoicesFixture.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<?php
/**
* This file is part of riesenia/persist-related-data package.
*
* Licensed under the MIT License
* (c) RIESENIA.com
*/
declare(strict_types=1);

namespace PersistRelatedData\Test\Fixture;

use Cake\ORM\Table;
use Cake\TestSuite\Fixture\TestFixture;

class InvoicesTable extends Table
{
public function initialize(array $config)
public function initialize(array $config): void
{
parent::initialize($config);

Expand All @@ -27,10 +35,10 @@ class InvoicesFixture extends TestFixture
{
public $fields = [
'id' => ['type' => 'integer'],
'a_field' => ['type' => 'string', 'default' => null, 'null' => true],
'contact_id' => ['type' => 'integer', 'default' => null, 'null' => true],
'contact_name' => ['type' => 'string', 'default' => null, 'null' => true],
'contact_address' => ['type' => 'string', 'default' => null, 'null' => true],
'a_field' => ['type' => 'string', 'length' => 255, 'null' => true],
'contact_id' => ['type' => 'integer', 'null' => true],
'contact_name' => ['type' => 'string', 'length' => 255, 'null' => true],
'contact_address' => ['type' => 'string', 'length' => 255, 'null' => true],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']]
]
Expand Down
Loading