Subresource feature /dummy/1/relatedDummies#904
Conversation
dd79f35 to
bc79541
Compare
| private $identifier; | ||
| private $childInherited; | ||
| private $attributes; | ||
| private $nestedOperation; |
There was a problem hiding this comment.
I don't think nestedOperation is a good name, but then again readableLink / writableLink are very confusing names as well...
Perhaps $subcollection / hasSubcollection?
| } | ||
|
|
||
| $operation = ['method' => 'GET', 'property' => $property]; | ||
| $this->addRoute($routeCollection, $resourceClass, strtolower($property).'_get', $operation, $resourceShortName, OperationTypes::NESTED); |
There was a problem hiding this comment.
I can imagine the route name as such: api_products_get_images_subcollection
So $operationName should probably be 'get_'.strtolower($property)`.
| '_api_resource_class' => $resourceClass, | ||
| sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, | ||
| sprintf('_api_%s_operation_name', $operationType) => $operationName, | ||
| '_api_resource_property' => $operation['property'] ?? null, |
There was a problem hiding this comment.
_api_subcollection_property_name?
7279804 to
253e55e
Compare
|
Okay, now it does work, I still have to fix the tests but as I made many changes I'd like to know if those are okay / not breaking anything:
The rest is pretty straightforward:
Please tell me what you think about the changes in general, is it too complex? Do you see things that can be simplified? Thanks! ping @api-platform/core-team |
dunglas
left a comment
There was a problem hiding this comment.
I think that there is a flaw in the general design. We should find a way to handle as many level as we need. Introducing a subcollection operation type looks not scalable enough. We should probably try to tweak the current item and collection operations to make them able to be nested.
| * @return string | ||
| */ | ||
| public function getResourceClass($value, string $resourceClass = null, bool $strict = false): string; | ||
| public function getResourceClass($value, array $context = null, bool $strict = false): string; |
There was a problem hiding this comment.
It's a BC break, maybe should we introduce a new method for that, but we can discuss this specific point later.
| $placeholder = ':id_'.$identifier; | ||
| $expression = sprintf('parentResourceClass.%s = %s', $identifier, $placeholder); | ||
|
|
||
| $where = !$where ? $expression : $with.' AND '.$expression; |
There was a problem hiding this comment.
$where ? "$with AND $expression" : $expression
?
| $where = null; | ||
| foreach ($identifiers as $identifier => $value) { | ||
| $placeholder = ':id_'.$identifier; | ||
| $expression = sprintf('parentResourceClass.%s = %s', $identifier, $placeholder); |
There was a problem hiding this comment.
I would use string concatenation here:
"parentResourceClass.$identifier = $placeholder"
| /** | ||
| * Tools that helps managing Identifiers (composite or regular) and related where clauses. | ||
| */ | ||
| class IdentifiersHelper |
There was a problem hiding this comment.
The class should be final, and we probably need to introduce an interface for it too.
There was a problem hiding this comment.
For the name, what about IdentifierGenerator or IdentifierManager?
| $identifierValues = [$id]; | ||
| $doctrineMetadataIdentifier = $manager->getClassMetadata($resourceClass)->getIdentifier(); | ||
|
|
||
| if (count($doctrineMetadataIdentifier) >= 2) { |
There was a problem hiding this comment.
2 <= count($doctrineMetadataIdentifier)
| } | ||
|
|
||
| $operation = ['method' => 'GET', 'property' => $property, 'subcollection' => $subcollection]; | ||
| $this->addRoute($routeCollection, $resourceClass, 'get_'.strtolower($property), $operation, $resourceShortName, OperationTypes::SUBCOLLECTION); |
There was a problem hiding this comment.
There is a high risk of conflicts here, the route name must include the name of the parent resource.
| '_api_resource_class' => $resourceClass, | ||
| sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, | ||
| sprintf('_api_%s_operation_name', $operationType) => $operationName, | ||
| '_api_subcollection_property_name' => $operation['property'] ?? null, |
There was a problem hiding this comment.
Wr should not add those keys at all if it's not a sub collection.
| * | ||
| * @author Antoine Bluchet <soyuka@gmail.com> | ||
| */ | ||
| interface SubcollectionDataProviderInterface |
There was a problem hiding this comment.
I'm not sure that we need a new interface. Can't we refactor the current set of interfaces (and implementations) to allow a recursive pattern like /users/2/posts/1/comments?
There was a problem hiding this comment.
We might but the dataprovides should understand that we "need the collection belonging to an item", and that's very different from both of the current implementations IMO.
src/Util/OperationTypes.php
Outdated
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace ApiPlatform\Core\Util; |
There was a problem hiding this comment.
We should find a better name, maybe can we just move this class in ApiPlatform\Core.
Yes I kinda agree with you and you would like to end up with something like: I'll try to dig deeper into this but we definitely need a different DataProvider for exactly this kind of operation. Just chaining Item/Collection would be really bad in performances. I'll do a separated Pull request tomorrow to get rid of that IdentifierGenerator / IdentifierManager or whatever. Thoughts for later |
Let's take your example:
I think this is how it should be: |
e94d28f to
e381bd9
Compare
|
Please don't mind the last commit and this comment I'm just keeping some notes. |
ec8312a to
e721183
Compare
|
|
||
| $queryBuilder->where( | ||
| $queryBuilder->expr()->in('o', $previousQueryBuilder->getDQL()) | ||
| ); |
There was a problem hiding this comment.
IMO this is the best way to do what we want because we force the SQL execution plan to go through indexes on doctrine identifiers. The other solution would be to use joins.
This transforms to (pseudo-dql):
SELECT thirdLevel
WHERE thirdLevel IN (
SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN (
SELECT relatedDummies FROM Dummy WHERE Dummy = ?
)
)
There was a problem hiding this comment.
Maybe can you add this as a comment in the code for future readers?
src/Api/ResourceClassResolver.php
Outdated
| return $resourceClass; | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Instead of BC break I chose to duplicate the method, I think we should deprecate the old one, let me know if you've a better idea.
There was a problem hiding this comment.
In fact I don't need this, back to the original method.
1eb2e49 to
3a2147c
Compare
|
|
4012a95 to
c7c98fa
Compare
|
Either i am completely missunderstanding this feature, or there are some major flaws in it. Consider the following classes: /**
* @API\ApiResource
* @ORM\Entity
*/
class Container
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="guid")
*
* @var string UUID
*/
private $id;
/**
* @API\ApiProperty(subresource=true)
* @ORM\OneToMany(
* targetEntity="Node",
* mappedBy="container",
* indexBy="serial",
* fetch="LAZY",
* cascade={},
* orphanRemoval=false
* )
* @ORM\OrderBy({"serial"="ASC"})
*
* @var Collection|Node[]
*/
private $nodes;
/**
* @return string
*/
public function getId(): ?string
{
return $this->id;
}
/**
* @return array|Node[]
*/
public function getNodes(): array
{
return $this->nodes->toArray();
}
}
/**
* @API\ApiResource
* @ORM\Entity
*/
class Node
{
/**
* @ORM\Id
* @ORM\ManyToOne(targetEntity="Container", fetch="LAZY")
* @ORM\JoinColumn(name="container_id", referencedColumnName="id", onDelete="RESTRICT")
*
* @var Container
*/
private $container;
/**
* @ORM\Id
* @ORM\Column(name="serial", type="integer")
*
* @var integer Node serial
*/
private $serial;
/**
*/
public function __construct()
{
}
public function setContainer(Container $container): void
{
$this->container= $container;
$this->serial = $container->nextSerial();
}
/**
*
* @return integer
*/
public function getSerial(): int
{
return $this->serial;
}
}invoking but i would expect a query more like this: any hints? |
|
@backbone87 thanks for your interest in this pull request. I'm currently reproducing your case in an intergration test. It's possible that there are uncovered parts here, because relations between resources can differ a lot according to the use cases.
Your choice of words is a bit strong here ^^. I don't think you misunderstood the feature:
To me the following DQL looks correct: SELECT o FROM Node o -- select nodes
WHERE o IN( -- where nodes are
SELECT IDENTITY(id_a1.nodes) -- the nodes
FROM Container id_a1
WHERE id_a1.id = :id_p1 -- belonging to this container
)The second one is also valid, but it assumes that there is a two-way relationship (ie container is stored in node). Anyway, I'm investigating this! |
|
Thanks @backbone87 this was a really interesting case it had:
I've fixed your issue and it works great through behat. Could you give it a try? Thanks! |
a8764a5 to
bef19e0
Compare
|
After digging deeper into the code, idk if this PR uses the right approach. A configuration scheme i would like to see: /**
* @ApiProperty(
* resource=@ApiResource(
* collectionValued=true|false,
* collectionOperations={ ... }, // invalid when collectionValued = false
* itemOperations={ ... }
* )
* )
*/
private $nodes; |
|
It's not really necessary to have to specify a subresource What bother's you with the current implementation? Keep in mind that for complex use cases, it'll always be easier to set up filters and custom operations (or a custom data provider). |
bef19e0 to
e50f163
Compare
|
Your changes do work with this use case. Anyway, i think we should aim at getting full operation support (incl custom operations) on subresources and in combination with that: disable a resource being top lvl (or in terms of the given use case: remove /nodes) |
|
our communication is somewhat deferred ;) i am on gitter, if you want to have chat more directly another problem with current configuration scheme: i cant name the paths properly (its autogenerated for subresources). Since i have to use german resources names for my current project the english inflector and pluralizer generates completely unuseable routes. |
2480c49 to
dcf00bc
Compare
Hydra @id now matches subresource IRI Rebase was hard, added a feature (temp commit)
dcf00bc to
959381b
Compare
|
ping @api-platform/core-team can someone review phpstan fixes in the last commit? Thanks! |
| */ | ||
| public static function getOperationType($operationType): string | ||
| { | ||
| if (is_bool($operationType)) { |
There was a problem hiding this comment.
To prevent the PHPStan error you may use if( !is_string(($operationType)) {.
There was a problem hiding this comment.
hmm okay though it should be correct like this! I'll change it thx.
There was a problem hiding this comment.
Okay so I did try !is_string but I get the same error :| I'll just ignore this one for now as this is a temporary class anyway.
|
Don't forget the doc PR :) |
|
@dunglas I am using the annotation |
|
Hi @cocciagialla, be careful when you update to the next beta release it'll be Also, I'm currently working on this in #1245. I suggest that you subscribe to the issue to know when it'll be ready :). |
|
There is a way that we can change the sub-queries made them in SubResourceDataProvider! Right now it's possbile to change ou well add other conditions to main queryBuilder but the previousBuilder made them into SubResourceDataProvider we can't hook into them with any type of extension? thanks |
…ation Subresource feature /dummy/1/relatedDummies
Note:
I didn't implement the DataProvider yet! I'm just proposing those first changes to see if you have comments/improvements on the way I handled the nested behavior!Related issues/PR:
#885
#634
What should this PR end up doing:
Allow
/entity/{id}/associationoperations when a flag on a resource's property is set. For example:You can chain those and have
/api/dummy/1/related_dummies/2/third_levels.Changes:
subcollection(boolean) on the PropertyMetadataApiPlatform\Util\OperationTypes(maybe the path isn't the best one, suggestions?) to ease the handling of operation typesTODO