Skip to content

Commit 1eadf82

Browse files
committed
Implement add operation for email values in Complex class and add corresponding tests Fix #119
1 parent 26c41e1 commit 1eadf82

File tree

2 files changed

+171
-4
lines changed

2 files changed

+171
-4
lines changed

src/Attribute/Complex.php

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,24 @@ public function patch($operation, $value, Model &$object, Path $path = null, $re
102102
}
103103
}
104104

105-
if (empty($matchedIndexes)) {
106-
return;
107-
}
108-
109105
$attributeNames = $path?->getAttributePath()?->getAttributeNames() ?? [];
110106
$modified = false;
111107

108+
if (empty($matchedIndexes)) {
109+
if ($operation === 'add') {
110+
$newElement = $this->createElementFromFilter($filterNode, $attributeNames, $value);
111+
112+
if ($newElement !== null) {
113+
$currentValues[] = $newElement;
114+
$modified = true;
115+
}
116+
}
117+
118+
if (!$modified) {
119+
return;
120+
}
121+
}
122+
112123
foreach ($matchedIndexes as $index) {
113124
if (empty($attributeNames)) {
114125
if ($operation === 'remove') {
@@ -638,4 +649,78 @@ private function restoreStructure(array $original, array $normalized): array
638649

639650
return $result;
640651
}
652+
653+
private function createElementFromFilter(AstFilter $filter, array $attributePath, mixed $value): ?array
654+
{
655+
$base = $this->extractAssignmentsFromFilter($filter);
656+
657+
if (!empty($attributePath)) {
658+
$base = $this->setNestedValue($base, $attributePath, $value);
659+
} elseif (is_array($value)) {
660+
$base = array_replace_recursive($base, $this->normalizeElement($value));
661+
} else {
662+
return null;
663+
}
664+
665+
return $this->normalizeElement($base);
666+
}
667+
668+
private function extractAssignmentsFromFilter(AstFilter $filter): array
669+
{
670+
if ($filter instanceof ComparisonExpression) {
671+
if (strtolower($filter->operator) !== 'eq') {
672+
return [];
673+
}
674+
675+
$attributeNames = $filter->attributePath->getAttributeNames();
676+
677+
if (empty($attributeNames)) {
678+
return [];
679+
}
680+
681+
return $this->setNestedValue([], $attributeNames, $filter->compareValue);
682+
}
683+
684+
if ($filter instanceof Conjunction) {
685+
$result = [];
686+
687+
foreach ($filter->getFactors() as $factor) {
688+
$result = array_replace_recursive($result, $this->extractAssignmentsFromFilter($factor));
689+
}
690+
691+
return $result;
692+
}
693+
694+
if ($filter instanceof AstValuePath) {
695+
$nested = $this->extractAssignmentsFromFilter($filter->getFilter());
696+
697+
$attributeNames = $filter->getAttributePath()->getAttributeNames();
698+
699+
return $this->setNestedValue([], $attributeNames, $nested);
700+
}
701+
702+
return [];
703+
}
704+
705+
private function setNestedValue(array $array, array $path, mixed $value): array
706+
{
707+
if (empty($path)) {
708+
return is_array($value) ? array_replace_recursive($array, $value) : $array;
709+
}
710+
711+
$segment = array_shift($path);
712+
713+
if (!array_key_exists($segment, $array) || !is_array($array[$segment])) {
714+
$array[$segment] = [];
715+
}
716+
717+
if (empty($path)) {
718+
$array[$segment] = $value;
719+
return $array;
720+
}
721+
722+
$array[$segment] = $this->setNestedValue($array[$segment], $path, $value);
723+
724+
return $array;
725+
}
641726
}

tests/EmailValuePathAddTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace ArieTimmerman\Laravel\SCIMServer\Tests;
4+
5+
use ArieTimmerman\Laravel\SCIMServer\Attribute\Complex;
6+
use ArieTimmerman\Laravel\SCIMServer\Parser\Parser;
7+
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;
8+
use ArieTimmerman\Laravel\SCIMServer\Tests\Model\User;
9+
use Illuminate\Database\Eloquent\Model;
10+
11+
class EmailValuePathAddTest extends TestCase
12+
{
13+
public function testAddOperationUpdatesEmailValue(): void
14+
{
15+
$user = User::query()->firstOrFail();
16+
17+
$payload = [
18+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
19+
'Operations' => [
20+
[
21+
'op' => 'Add',
22+
'path' => 'emails[type eq "other"].value',
23+
'value' => '[email protected]',
24+
],
25+
],
26+
];
27+
28+
$response = $this->patchJson('/scim/v2/Users/' . $user->id, $payload);
29+
30+
$response->assertStatus(200);
31+
32+
$user->refresh();
33+
34+
$this->assertSame('[email protected]', $user->email);
35+
}
36+
37+
public function testAddOperationCreatesNewElementWhenFilterMatchesNone(): void
38+
{
39+
$model = new class extends Model {
40+
protected $table = 'users';
41+
public $timestamps = false;
42+
};
43+
44+
$model->emails = [
45+
['value' => '[email protected]', 'type' => 'work'],
46+
];
47+
48+
$attribute = $this->makeEmailAttribute();
49+
50+
$path = Parser::parse('emails[type eq "other"].value');
51+
$path->shiftValuePathAttributes();
52+
53+
$attribute->patch('add', '[email protected]', $model, $path);
54+
55+
$this->assertCount(2, $model->emails);
56+
$this->assertSame('[email protected]', $model->emails[0]['value']);
57+
$this->assertSame('other', $model->emails[1]['type']);
58+
$this->assertSame('[email protected]', $model->emails[1]['value']);
59+
}
60+
61+
private function makeEmailAttribute(): Complex
62+
{
63+
return new class('emails') extends Complex {
64+
public function __construct($name)
65+
{
66+
parent::__construct($name);
67+
$this->setMultiValued(true);
68+
}
69+
70+
protected function doRead(&$object, $attributes = [])
71+
{
72+
return $object->{$this->name} ?? [];
73+
}
74+
75+
public function replace($value, Model &$object, Path $path = null, $removeIfNotSet = false)
76+
{
77+
$object->{$this->name} = $value;
78+
$this->dirty = true;
79+
}
80+
};
81+
}
82+
}

0 commit comments

Comments
 (0)