Skip to content

Commit f477c61

Browse files
authored
Merge branch '5.x' into ServiceLocator
2 parents d97aa23 + 1d541bf commit f477c61

25 files changed

+1336
-683
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,9 @@ jobs:
1818
php: "8.4"
1919
- laravel: 12
2020
php: "8.3"
21-
- laravel: 10
22-
php: "8.2"
2321

24-
- laravel: 9
22+
- laravel: 11
2523
php: "8.2"
26-
- laravel: 9
27-
php: "8.1"
28-
- laravel: 9
29-
php: "8.0"
30-
31-
- laravel: 8
32-
php: "8.1"
33-
- laravel: 8
34-
php: "8.0"
35-
- laravel: 8
36-
php: "7.4"
37-
- laravel: 8
38-
php: "7.3"
3924
name: PHP ${{ matrix.php }} / Laravel ${{ matrix.laravel }}
4025
steps:
4126
- uses: actions/checkout@v3
@@ -173,7 +158,7 @@ jobs:
173158
COMPOSER_PROCESS_TIMEOUT: 10000
174159
run: composer analyse
175160
- name: Report coverage
176-
uses: paambaati/codeclimate-action@v3.2.0
161+
uses: paambaati/codeclimate-action@v9.0.0
177162
env:
178163
CC_TEST_REPORTER_ID: 1be98f680ca97065e8a18ad2df18e67210033bb0708b5b70e4d128b035b0cb45
179164
COMPOSER_PROCESS_TIMEOUT: 10000

composer.lock

Lines changed: 836 additions & 651 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpunit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
</testsuites>
1010
<php>
1111
<env name="DATABASE_URL" value="sqlite:///:memory:"/>
12+
<env name="APP_ENV" value="testing"/>
1213
</php>
1314
<source>
1415
<include>

src/Drivers/EloquentEntitySet.php

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Flat3\Lodata\Exception\Protocol\ConfigurationException;
2828
use Flat3\Lodata\Exception\Protocol\InternalServerErrorException;
2929
use Flat3\Lodata\Exception\Protocol\NotFoundException;
30+
use Flat3\Lodata\Exception\Protocol\NotImplementedException;
3031
use Flat3\Lodata\Facades\Lodata;
3132
use Flat3\Lodata\Helper\Discovery;
3233
use Flat3\Lodata\Helper\JSON;
@@ -42,6 +43,7 @@
4243
use Flat3\Lodata\Interfaces\EntitySet\PaginationInterface;
4344
use Flat3\Lodata\Interfaces\EntitySet\QueryInterface;
4445
use Flat3\Lodata\Interfaces\EntitySet\ReadInterface;
46+
use Flat3\Lodata\Interfaces\EntitySet\RelationshipInterface;
4547
use Flat3\Lodata\Interfaces\EntitySet\SearchInterface;
4648
use Flat3\Lodata\Interfaces\EntitySet\UpdateInterface;
4749
use Flat3\Lodata\Interfaces\TransactionInterface;
@@ -56,6 +58,8 @@
5658
use Illuminate\Database\Eloquent\Builder;
5759
use Illuminate\Database\Eloquent\Model;
5860
use Illuminate\Database\Eloquent\Relations\BelongsTo;
61+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
62+
use Illuminate\Database\Eloquent\Relations\HasMany;
5963
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
6064
use Illuminate\Database\Eloquent\Relations\HasOne;
6165
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
@@ -76,7 +80,7 @@
7680
* Eloquent Entity Set
7781
* @package Flat3\Lodata\Drivers
7882
*/
79-
class EloquentEntitySet extends EntitySet implements CountInterface, CreateInterface, DeleteInterface, ExpandInterface, FilterInterface, OrderByInterface, PaginationInterface, QueryInterface, ReadInterface, SearchInterface, TransactionInterface, UpdateInterface, ComputeInterface
83+
class EloquentEntitySet extends EntitySet implements CountInterface, CreateInterface, DeleteInterface, ExpandInterface, FilterInterface, OrderByInterface, PaginationInterface, QueryInterface, ReadInterface, SearchInterface, TransactionInterface, UpdateInterface, ComputeInterface, RelationshipInterface
8084
{
8185
use SQLConnection;
8286
use SQLOrderBy;
@@ -195,6 +199,80 @@ protected function setModelAttributes(Model $model, PropertyValues $propertyValu
195199
}, $model);
196200
}
197201

202+
public function unlink(Entity $source, Property $property, Entity $target): Entity
203+
{
204+
if (get_class($source) !== $this->entityClass) {
205+
throw new InternalServerErrorException(
206+
'unexpected_link_source',
207+
'The link source for this entity valid.'
208+
);
209+
}
210+
211+
/** @var $sourceModel Model */
212+
$sourceModel = $source->getSource();
213+
/** @var $targetModel Model */
214+
$targetModel = $target->getSource();
215+
if (!($sourceModel instanceof Model && $targetModel instanceof Model)) {
216+
throw new InternalServerErrorException(
217+
'can_only_link_eloquent',
218+
'The link source or target is not supported for this entity set.'
219+
);
220+
}
221+
222+
$navigationRef = $this->getPropertySourceName($property);
223+
224+
$rel = $sourceModel->{$navigationRef}();
225+
if ($rel instanceof BelongsToMany) {
226+
$rel->detach($targetModel);
227+
} elseif ($rel instanceof BelongsTo) {
228+
$rel->dissociate();
229+
} elseif ($rel instanceof HasMany) {
230+
$targetModel->{$rel->getForeignKeyName()} = null;
231+
$targetModel->save();
232+
} else {
233+
throw new NotImplementedException();
234+
}
235+
236+
return $source;
237+
}
238+
239+
public function link(Entity $source, Property $property, Entity $target): Entity
240+
{
241+
if (get_class($source) !== $this->entityClass) {
242+
throw new InternalServerErrorException(
243+
'unexpected_link_source',
244+
'The link source for this entity valid.'
245+
);
246+
}
247+
248+
/** @var $sourceModel Model */
249+
$sourceModel = $source->getSource();
250+
/** @var $targetModel Model */
251+
$targetModel = $target->getSource();
252+
if (!($sourceModel instanceof Model && $targetModel instanceof Model)) {
253+
throw new InternalServerErrorException(
254+
'can_only_link_eloquent',
255+
'The link source or target is not supported for this entity set.'
256+
);
257+
}
258+
259+
$navigationRef = $this->getPropertySourceName($property);
260+
261+
$rel = $sourceModel->{$navigationRef}();
262+
if ($rel instanceof BelongsToMany) {
263+
$rel->syncWithoutDetaching($targetModel);
264+
} elseif ($rel instanceof BelongsTo) {
265+
$rel->associate($targetModel);
266+
} elseif ($rel instanceof HasMany) {
267+
$targetModel->{$rel->getForeignKeyName()} = $sourceModel->{$rel->getLocalKeyName()};
268+
$targetModel->save();
269+
} else {
270+
throw new NotImplementedException();
271+
}
272+
273+
return $source;
274+
}
275+
198276
/**
199277
* Update an Eloquent model
200278
* @param PropertyValue $key Model key
@@ -534,6 +612,7 @@ public function discoverRelationship(
534612
}
535613

536614
$navigationProperty = (new NavigationProperty($name ?? $method, $right->getType()))->setCollection(true);
615+
$this->setPropertySourceName($navigationProperty, $method);
537616

538617
if ($description) {
539618
$navigationProperty->addAnnotation(new Description($description));

src/Entity.php

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
use Flat3\Lodata\Helper\PropertyValue;
1919
use Flat3\Lodata\Interfaces\ContextInterface;
2020
use Flat3\Lodata\Interfaces\EntitySet\DeleteInterface;
21+
use Flat3\Lodata\Interfaces\EntitySet\RelationshipInterface;
2122
use Flat3\Lodata\Interfaces\EntitySet\UpdateInterface;
2223
use Flat3\Lodata\Interfaces\PipeInterface;
23-
use Flat3\Lodata\Interfaces\ResourceInterface;
24-
use Flat3\Lodata\Interfaces\ResponseInterface;
2524
use Flat3\Lodata\Transaction\MetadataContainer;
2625
use Illuminate\Contracts\Container\BindingResolutionException;
2726
use Illuminate\Http\Request;
@@ -250,16 +249,39 @@ public function getResourceUrl(Transaction $transaction): string
250249
*/
251250
public function delete(Transaction $transaction, ?ContextInterface $context = null): void
252251
{
253-
$entitySet = $this->entitySet;
252+
if ($this->parent && $this->usesReferences()) {
253+
$this->deleteRelationship($transaction, $context);
254+
return;
255+
}
254256

255-
if (!$entitySet instanceof DeleteInterface) {
257+
if (!$this->entitySet instanceof DeleteInterface) {
256258
throw new NotImplementedException('entityset_cannot_delete', 'This entity set cannot delete');
257259
}
258260

259261
Gate::delete($this, $transaction)->ensure();
260262
$transaction->assertIfMatchHeader($this->getETag());
261263

262-
$entitySet->delete($this->getEntityId());
264+
$this->entitySet->delete($this->getEntityId());
265+
}
266+
267+
public function deleteRelationship(Transaction $transaction, ?ContextInterface $context = null): void
268+
{
269+
if (!$this->parent || !$this->usesReferences()) {
270+
throw new BadRequestException();
271+
}
272+
273+
Gate::update($this, $transaction)->ensure();
274+
275+
/** @var Entity $source */
276+
$source = $this->parent->getParent();
277+
$sourceEntitySet = $source->getEntitySet();
278+
279+
if ($sourceEntitySet instanceof RelationshipInterface) {
280+
$sourceEntitySet->unlink($source, $this->parent->getProperty(), $this);
281+
return;
282+
}
283+
284+
throw new MethodNotAllowedException('entityset_cannot_link', 'This entity set cannot update relationships');
263285
}
264286

265287
/**
@@ -302,6 +324,35 @@ public function patch(Transaction $transaction, ?ContextInterface $context = nul
302324
return $entity->get($transaction, $context);
303325
}
304326

327+
public function post(Transaction $transaction, ?ContextInterface $context = null): Response
328+
{
329+
if ($this->parent && $this->usesReferences()) {
330+
return $this->postRelationships($transaction, $context);
331+
}
332+
333+
throw new MethodNotAllowedException();
334+
}
335+
336+
public function postRelationships(Transaction $transaction, ?ContextInterface $context = null): Response
337+
{
338+
if (!$this->parent || !$this->usesReferences()) {
339+
throw new BadRequestException();
340+
}
341+
342+
Gate::update($this, $transaction)->ensure();
343+
344+
/** @var Entity $source */
345+
$source = $this->parent->getParent();
346+
$sourceEntitySet = $source->getEntitySet();
347+
348+
if ($sourceEntitySet instanceof RelationshipInterface) {
349+
$sourceEntitySet->link($source, $this->parent->getProperty(), $this);
350+
throw new NoContentException('success', 'Relationship was created');
351+
}
352+
353+
throw new MethodNotAllowedException('entityset_cannot_link', 'This entity set cannot update relationships');
354+
}
355+
305356
/**
306357
* Read this entity
307358
* @param Transaction $transaction Related transaction
@@ -339,6 +390,9 @@ public function response(Transaction $transaction, ?ContextInterface $context =
339390
case Request::METHOD_PUT:
340391
return $this->patch($transaction, $context);
341392

393+
case Request::METHOD_POST:
394+
return $this->post($transaction, $context);
395+
342396
case Request::METHOD_DELETE:
343397
$this->delete($transaction, $context);
344398
throw new NoContentException('deleted', 'Content was deleted');

src/EntitySet.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,19 @@ public static function pipe(
536536
);
537537
}
538538

539+
$keyValue = self::idToKeyProperty($id, $entitySet, $transaction);
540+
541+
return $entitySet->negotiateUpsert($keyValue, $transaction, $nextSegment);
542+
}
543+
544+
/**
545+
* @param string $id
546+
* @param EntitySet $entitySet
547+
* @param Transaction|null $transaction
548+
* @return PropertyValue
549+
*/
550+
public static function idToKeyProperty($id, EntitySet $entitySet, ?Transaction $transaction = null): PropertyValue
551+
{
539552
$lexer = new Lexer($id);
540553

541554
// Get the default key property
@@ -555,7 +568,7 @@ public static function pipe(
555568

556569
if ($longFormKey && $lexer->maybeChar('=')) {
557570
// Test for referenced value syntax
558-
if ($lexer->maybeChar('@')) {
571+
if ($transaction && $lexer->maybeChar('@')) {
559572
$referencedKey = $lexer->identifier();
560573
$referencedValue = $transaction->getParameterAlias($referencedKey);
561574
$lexer = new Lexer($referencedValue);
@@ -588,7 +601,9 @@ public static function pipe(
588601
$keyValue->setProperty($keyProperty);
589602

590603
try {
591-
$keyValue->setValue($lexer->type($keyProperty->getPrimitiveType()));
604+
$prim = $keyProperty->getPrimitiveType();
605+
$type = $lexer->type($prim);
606+
$keyValue->setValue($type);
592607
} catch (LexerException $e) {
593608
throw (new BadRequestException(
594609
'invalid_identifier_value',
@@ -597,7 +612,7 @@ public static function pipe(
597612
))->lexer($lexer);
598613
}
599614

600-
return $entitySet->negotiateUpsert($keyValue, $transaction);
615+
return $keyValue;
601616
}
602617

603618
/**
@@ -606,7 +621,7 @@ public static function pipe(
606621
* @param Transaction $transaction Transaction
607622
* @return PipeInterface
608623
*/
609-
public function negotiateUpsert(PropertyValue $entityId, Transaction $transaction): PipeInterface
624+
public function negotiateUpsert(PropertyValue $entityId, Transaction $transaction, ?string $nextSegment = null): PipeInterface
610625
{
611626
$key = $this->getType()->getKey();
612627

@@ -627,6 +642,13 @@ public function negotiateUpsert(PropertyValue $entityId, Transaction $transactio
627642
}
628643
}
629644

645+
if ($nextSegment && $nextSegment === '$ref') {
646+
throw new NotFoundException(
647+
'entity_not_found',
648+
'cannot modify relationship with missing entity'
649+
);
650+
}
651+
630652
if ($key->isComputed()) {
631653
throw new BadRequestException(
632654
'cannot_upsert_computed_key',

src/Expression/Lexer.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,17 @@ public function operator(string $symbol): ?string
554554
});
555555
}
556556

557+
/**
558+
* Maybe match whitespace
559+
* @return string|null
560+
*/
561+
public function maybeMatchingParenthesis(): ?string
562+
{
563+
return $this->with(function () {
564+
return $this->matchingParenthesis();
565+
});
566+
}
567+
557568
/**
558569
* Match a string enclosed in matching parentheses
559570
* @return string

src/Helper/PropertyValue.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Flat3\Lodata\Exception\Protocol\BadRequestException;
1717
use Flat3\Lodata\Exception\Protocol\MethodNotAllowedException;
1818
use Flat3\Lodata\Exception\Protocol\NoContentException;
19+
use Flat3\Lodata\Exception\Protocol\NotFoundException;
1920
use Flat3\Lodata\Expression\Lexer;
2021
use Flat3\Lodata\GeneratedProperty;
2122
use Flat3\Lodata\Interfaces\ContextInterface;
@@ -334,14 +335,28 @@ public static function pipe(
334335
$navigationRequest = new NavigationRequest();
335336
$navigationRequest->setOuterRequest($transaction->getRequest());
336337
$navigationRequest->setNavigationProperty($property);
338+
339+
if ($propertyParams = $lexer->maybeMatchingParenthesis()) {
340+
$navigationRequest->setNavigationParams($propertyParams);
341+
}
342+
337343
$property->generatePropertyValue($transaction, $navigationRequest, $argument);
338344
}
339345

340346
if ($property instanceof GeneratedProperty) {
341347
$property->generatePropertyValue($argument);
342348
}
343349

344-
return $argument->getPropertyValues()->get($property);
350+
$target = $argument->getPropertyValues()->get($property);
351+
352+
if ($property instanceof NavigationProperty && $nextSegment === '$ref' && !($target->getValue() instanceof Entity)) {
353+
throw new NotFoundException(
354+
'entity_not_found',
355+
'cannot modify relationship with missing entity'
356+
);
357+
}
358+
359+
return $target;
345360
}
346361

347362
public function emitJson(Transaction $transaction): void

0 commit comments

Comments
 (0)