New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Subresource feature /dummy/1/relatedDummies #904
Subresource feature /dummy/1/relatedDummies #904
Conversation
dd79f35
to
bc79541
Compare
@@ -31,8 +31,9 @@ | |||
private $identifier; | |||
private $childInherited; | |||
private $attributes; | |||
private $nestedOperation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can imagine the route name as such: api_products_get_images_subcollection
So $operationName
should probably be 'get_'.strtolower($property)`.
@@ -156,7 +173,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas | |||
'_controller' => $controller, | |||
'_format' => null, | |||
'_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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
* | ||
* @throws InvalidArgumentException | ||
* | ||
* @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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class should be final
, and we probably need to introduce an interface for it too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a high risk of conflicts here, the route name must include the name of the parent resource.
@@ -156,7 +173,9 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas | |||
'_controller' => $controller, | |||
'_format' => null, | |||
'_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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can you add this as a comment in the code for future readers?
src/Api/ResourceClassResolver.php
Outdated
@@ -63,6 +63,39 @@ public function getResourceClass($value, string $resourceClass = null, bool $str | |||
/** | |||
* {@inheritdoc} | |||
*/ | |||
public function getResourceClassFromContext($value, array $context = null, bool $strict = false): string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent the PHPStan error you may use if( !is_string(($operationType)) {
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm okay though it should be correct like this! I'll change it thx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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}/association
operations 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