Translate MongoDB Aggregation $all (JS) into Doctrine ODM Aggregation (PHP) - php

I am currently trying to convert this JS code:
db.Issue.aggregate( [
{ // Filter out issues that have been worked on after our given week = All worklogs muste have been before date
$match : {
worklogs: { $all: [
{
"$elemMatch" : {
date: { $lte: endDate }
}
},
] }
}
}
] )
(Based on official documentation: Use $all with $elemMatch
Into Doctrine ODM code using their aggregation builder:
$builder = $this->createAggregationBuilder();
$aggregation = $builder
->match()
->field('worklogs')
->all([
$builder->matchExpr()->elemMatch(
$builder->expr()->field('date')->lte($week->getEndDate())
)
])
;
However, apparently I am unable to match the $all correctly - meaning in a way that allows me to ensure that all entries in a collection fulfilled the requirements checked by $elemMatch.
The original MongoDB query in JS seems to do the trick, but I cannot manage to receive the same results in PHP. By now I am not even sure Doctrine can handle that combination of $all and $elemMatch.
UPDATE:
Thank you all for your comments so far! I am however still struggling with the current query syntax (in PHP!) for the $not / ->not() operator. I have already checked the docs here: Doctrine ODM Docs but could not find anything useful. (Please also note, that I am using version 2.0 here).
Right now my query looks like:
->match()
->field('worklogs')
->not([
$builder->matchExpr()->elemMatch(
$builder->matchExpr()->field('date')->gt($week->getEndDate())
)
])
But leads to this error:
1) App\Tests\Repository\IssueRepositoryTest::testGetEstimationsPerWeek
MongoDB\Driver\Exception\CommandException: $not needs a regex or a document
/var/www/html/vendor/mongodb/mongodb/src/Operation/Aggregate.php:263
/var/www/html/vendor/mongodb/mongodb/src/Collection.php:223
/var/www/html/vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php:168
/var/www/html/vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php:35
/var/www/html/src/Repository/IssueRepository.php:85

So thanks to the help of #neil-lunn, I was able to figure out the correct query:
->match()
->field('worklogs')
->not(
$builder->matchExpr()->elemMatch(
$builder->matchExpr()->field('date')->gt($week->getEndDate())
)
)

Related

php: how to parse graphQl query string to get the operation names

I would like to allow certain graphQl operations only for certain api users based on a confguration. Our stack is Symfony 6.2 + overblog/GraphQLBundle.
My current approach is to check in the authenticate method of a custom authenticator, if the current operation is cleared in the allowed operations config of the user. For this I would like to parse the graphql query into a kind of array, that I can interpret easily.
Anybody knows how this can be done? I was scanning the underlying webonyx/graphql-php library, but can not see how they do it.
As a simple example:
query myLatestPosts($followablesToFilter: [FollowableInput], $limit: Int, $offset: Int) {
my_latest_posts(followablesToFilter: $followablesToFilter, limit: $limit, offset: $offset) {
...PostFragment
__typename
}
my_roles
}
From this I would like to retrieve the operations my_latest_posts and my_roles.
Update 1
it's probably possible to write a simple lexer utilising preg_split - I'm just hesitating, as I'm sure someone has done this already... The spec appears to be well defined.
Alright, so it turned out webonyx/graphql-php has indeed all the lowlevel function needed for this :D. Especially the Visitor is very useful here.
With this code you can drill down the query to get the operation names (or selections, as they are called in the code)
This works for me:
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
// whatever comes before
// ...
$graphQlRequest = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$operations = [];
Visitor::visit(Parser::parse($graphQlRequest['query']), [
NodeKind::OPERATION_DEFINITION => function ($node) use (&$operations) {
// $node contains the whole graphQL query in a structured way
$selections = array_map(function ($selection) {
return $selection['name']['value'];
}, $node->toArray()['selectionSet']['selections']);
foreach ($selections as $selection) {
$operations[] = $selection;
}
return Visitor::stop();
}]
);
print_r($operations);
The result of $operations is then for my example above:
Array
(
[0] => my_latest_posts
[1] => my_roles
)
And this information is all I need to decide weather the user should have access to the endpoint or not.

Add new field to a MongoDB document parsing from String to Int using updateMany

In my MongoDB collection, all documents contain a mileage field which currently is a string. Using PHP, I'd like to add a second field which contains the same content, but as an integer value. Questions like How to change the type of a field? contain custom MongoDB code which I don't want to run using PHP, and questions like mongodb php Strings to float values retrieve all documents and loop over them.
Is there any way to use \MongoDB\Operation\UpdateMany for this, as this would put all the work to the database level? I've already tried this for static values (like: add the same string to all documents), but struggle with getting the data to be inserted from the collection itself.
Some further hints:
I'm looking for a pure PHP solution that does not rely on any binary to be called using exec. This should avoid installing more packages than needed on the PHP server
Currently, I have to use MongoDB in v4.0. Yes, that's not the most recent version, but I'm not in the position to perform an upgrade
Try this, please:
01) MongoDB Aggregate reference:
db.collectionName.aggregate(
[
{ "$addFields": {
"intField": { "$toInt": "$stringFieldName" }
}},
{ "$out": "collectionName" }
]
)
02) Possible PHP solution (Using as reference https://www.php.net/manual/en/mongocollection.aggregate.php):
$pipeline = array(
array(
'$addFields' => array(
'integerField' => array('$toInt' => '$mileage')
)
),
array(
'$out' => 'collection'
),
);
$updateResult = $collection->aggregate(pipeline);
You could use $set like this in 4.2 which supports aggregation pipeline in update.
$set stage creates a mileageasint based on the previous with $toInt value
db.collection.updateMany(
<query>,
[{ $set: { "mileageasint":{"$toInt":"$mileage" }}}],
...
)
Php Solution ( Using example from here)
$updateResult = $collection->updateMany(
[],
[['$set' => [ 'mileageasint' => [ '$toInt' => '$mileage']]]]
);

Doctrine MongoDB ODM - Return only matched embedded document using query builder

I am having trouble in returning only the matched embedded document using MongoDB ODM query builder in PHP. Each embedded document has a MongoID generated at the time of creation. Following is my document structure of collection Project:
{
"_id" : ObjectId("59f889e46803fa3713454b5d"),
"projectName" : "usecase-updated",
"classes" : [
{
"_id" : ObjectId("59f9d7776803faea30b895dd"),
"className" : "OLA"
},
{
"_id" : ObjectId("59f9d8ad6803fa4012b895df"),
"className" : "HELP"
},
{
"_id" : ObjectId("59f9d9086803fa4112b895de"),
"className" : "DOC"
},
{
"_id" : ObjectId("59f9d9186803fa4212b895de"),
"className" : "INVOC"
}
]
}
Now i want to retrieve from the database only the class from the classes embedded documents which meets my criteria (i.e. class with a specific id).This is how i am building the query:
$qb = $dm->createQueryBuilder('Documents\Project');
$projectObj = $qb
->field('id')->equals("59f889e46803fa3713454b5d")
->field('classes')->elemMatch(
$qb->expr()->field("id")->equals(new \MongoId("59f9d7776803faea30b895dd"))
)
->hydrate(false)
->getQuery()
->getSingleResult();
First i match with the project id then i match with the embedded document class id. I was expecting it to return only the embedded document of OLA like this:
{
"_id" : ObjectId("59f889e46803fa3713454b5d"),
"projectName" : "usecase-updated",
"classes" : [
{
"_id" : ObjectId("59f9d7776803faea30b895dd"),
"className" : "OLA"
}
]
}
But doctrine is returning the whole Project record (shown in the start of question).I also tried with aggregation query building with the $match aggregation still the results are same the query i created with aggregation builder is as follows:
$qb = $dm->createAggregationBuilder('Documents\Project');
$projectObj = $qb
->match()
->field('id')->equals($projectId)
->field('classes._id')->equals(new \MongoId($classId))
->execute()
->getSingleResult();
Can someone help me with regards to this issue? How can i build query that i get the desired result as mentioned above.
So after some trouble i am able to retrieve only the matched embedded document with the aggregation builder. So thanks to #Alex Bles comment i was able to think more on using aggregation functions and i found $filter array aggregation function and tried building query using that and the final query was like this:
$qb = $dm->createAggregationBuilder('Documents\Project');
$classObject = $qb->match()
->field("id", new \MongoId($projectId))
->project()
->field("classes")
->filter('$classes', 'class', $qb->expr()->eq('$$class._id', new \MongoId($classId)))
->execute()->getSingleResult();
So in the query first i matched with the _id of the project. then within that project i projected the results for the classes using the $filter array aggregation method. In the end i was able to get the embedded document filtered by their _id. Hope this will help someone in the future with same problem.

Remove item from array in a mongodb document using Doctrine ODM

I have a mongoDB document comprising the following structure:
{
"_id" : ObjectId("537b9731fa4634134c8b45aa"),
"kpis" : [
{
"id" : 4,
"value" : 3.78,
"entered" : Timestamp(1401377656, 9)
}
]
}
I want to remove ALL kpi documents where the id is x. This is quite simple to do on the database directly using the pull command:
db.lead.update({}, {$pull:{"kpis":{id:5}}}, {multi:true});
However my (several) attempts to match this syntax using the doctrine ODM have failed:
$qb->update()
->field('kpis')
->pull($qb->expr()->field('kpis.id')->equals($kpi->getId()))
->multiple(true)
->getQuery()
->execute();
// I've also tried..
$qb->update()
->field('kpis')
->pull($qb->expr()->field('kpis')->elemMatch(
$qb->expr()->field('kpis.id')->equals($kpi->getId())
))
->multiple(true)
->getQuery()
->execute();
Nothing is removed. Am I using the query builder correctly?
I believe you want to do the following:
$qb->update()
->field('kpis')->pull(array('id' => $kpi->getId()))
->multiple(true)
->getQuery()
->execute();
You can use the Query::debug() method to dump the actual query generated by this. You should expect the following in the dumped newObj field:
{ "$pull": { "kpis": { "id": <number> }}}
To explain why your previous examples didn't work:
->field('kpis')
->pull($qb->expr()->field('kpis.id')->equals($kpi->getId()))
Here, you're creating the following query:
{ "$pull": { "kpis": { "kpis.id": <number> }}}
This would only match:
If each embedded object in the top-level kpis array had an embedded kpis object within it, which in turn had an id field that matched the number.
If the embedded objects in the top-level kpis array had their own kpis arrays consisting of embedded objects with an id field that matched. This would be MongoDB's array element matching coming into play.
In your second example, you have:
->field('kpis')
->pull($qb->expr()->field('kpis')->elemMatch(
$qb->expr()->field('kpis.id')->equals($kpi->getId())
))
This would generate the following query:
{ "$pull": { "kpis": { "$elemMatch": { "kpis.id": <number> }}}}
I've never seen such syntax before, but I think the $elemMatch is superflous, since $pull already takes criteria to match an array element as its argument. $elemMatch is useful in a query when you're directly matching on an array field and don't mean to do a full equality match against the array elements (i.e. you want to provide criteria instead of an exact match).

Query mongodb with php-mongo

I am building a simple messaging system, and i have a collection in mongodb with documents like this:
{ "_id" : ObjectId("50ad003f9811e5bc5c000000"), "between" : [ "user1,user2,user3" ] }
I want to perform this query:
db.conversations.find({'between': ["user1,user2,user3"]});
to get this exact document back. This query works in mongo shell.
in php-mongo, i tried this:
$collection->find(array("between"=>array("user1", "user2", "user3")));
but it does not work.
What am i doing wrong ?
Wouldn't you want to do an In query here?
db.collection.find( { "between" : { $in : ["user1", "user2", "user3"] } } );
See In query here:
Mongo Advanced $in query
making your PHP query look like:
$collection->find(array("between"=>array("$in"=>array("user1", "user2", "user3"))));
//untested, should be something similar to this.
or if you're trying to find it exactly wouldn't you just be able to do:
$collection->find(array("between"=>array("user1,user2,user3")));
First of all when you are saving your data you have to use array not a string
{ "between" : [ "user1,user2,user3" ] }
this stores in "between" an array of one element "user1,user2,user3"
Basically when you do your query in shell everything is ok, because you are asking for a array with one element. But in php you are asking for an array of three elements.
So, when you save your data, most probably that is what you need :
{ "between" : [ "user1","user2","user3" ] }
and this will give you an array of three users.
Then read the documentation http://www.mongodb.org/display/DOCS/Advanced+Queries to and adjust your query depends on what you need: either the exact array or only some elements in the array
Have you tried:
$collection->find(array("between"=>"user1,user2,user3"));
or
$collection->find(array( "$elemMatch" => array( "between"=>"user1,user2,user3" ));
The $in operator is analogous to the SQL IN modifier, allowing you to specify an array of possible matches.
Consider the following example which uses the $or operator.
$collection->find([
'select' => ['$in' => ['option 1', 'option 2', 'option 3']]
]);
References

Categories