I've created a query in PHP which is used to add a user to an array. Right now it checks to see if they exist and if they don't it adds them to the array. Here's the full code:
try{ $this->users_db->update(
array(
'_id' => new MongoId($user_id) ,
new MongoId( $group_id ) => array('$nin'=>USER_GROUPS)
),
array(
'$push' => array(USER_GROUPS => array( GROUP_ID => new MongoId($group_id), USER_GROUP_NOTIFY => true ) )
)
); }
catch(Exception $e)
{ return false; }
The problem is that PHP is giving me the Warning "Illegal offset type" since MongoId() is an object and objects can't be used as keys in arrays. Any ideas about how to work around this?
I think you have the order of your "arguments" to $nin backwards. Your query is equivalent to something like this in the mongo shell:
db.users.update({_id: ObjectId("..."), ObjectId("..."): {$nin: ["user_groups"]}}, ...);
Which reads like English, left to right, when pronouncing "$nin" as "is not in". A more correct, by MongoDB's grammar, pronunciation is "does not contain", so your query is actually saying something like "where some ObjectId does not contain this array", which makes little sense when said out loud.
With that in mind, your query should look like:
db.users.update({_id: ObjectId("..."), user_groups: {$nin: [ObjectId("...")]}}, ...);
When running into issues like this with updates or removes, it's often useful to try the query spec portion as an argument to find() or findOne() to determine what's wrong there. Once you can find the document you want to update, you can re-write as a call to update(), remove(), etc.
Also, you should be aware that there is an $addToSet atomic operator which performs this sort of check for you atomically in the database. You could try:
db.users.update({_id: ObjectId("...")}, {$addToSet: {user_groups: ObjectId("...")}});
EDIT: For future reference to OP and other askers, see mongodb docs on query operators and mongodb docs on update operators.
Related
I am pretty sure this challenge has been solved by someone already but even searching with different words, I could not find a solution for this problem:
I try to give users the possibility to run certain functions of a class based on an argument like
service_class::do_this( "selection-argument" );
but the user shall be able to use "clear words" as well as "aliases" and even "well known" abbreviations or synonyms.
I use switch-case construction to call the "real" function.
Example: To get the contens of a folder, The user can use "getdir", "dir", "Directory", "getfolder", "getcontent", "content", "d-cont" and a number of more other "matching words" to start the function(s) underlaying and getting back the very same result.
Capture-ing lowercase/uppercase is simple. What I search for is an efficient way to capture all possible "variations" - that are, of course different number of variations for different functions called.
At the moment I use multiple "case "": lines after each other, but that makes the code quite long, and further I would like the user to be able to "enahnce" the recognition set for a certain function.
That's why I thought about "stripos" to determine first what "internal word" to use and only then run into the switch-case construction.
Anyone had that issue and can direct me to a "good and efficient" solution?
Seems that Stck-exchange itself had a similar challenge (https://codereview.stackexchange.com/tags/php/synonyms) ... maybe I can simply re-use the underlying code?
Thanks in advance and sorry if I overlooked a solution already posted.
You could use a database or array. Let's do the latter. So to determine whether an user wants to get a directory you would define an array like this:
$getDirVariants = ['getdir',
'dir',
'directory',
'getfolder',
'getcontent',
'content',
'd-cont'];
It is easy to add more of these arrays. To test the query word you would do:
$queryWord = strtolower($queryWord);
if (in_array($queryWord, $getDirVariants)) service_class::getDir(<arguments>);
elseif (in_array($queryWord, $deleteVariants)) service_class::delete(<arguments>);
You can easily add to the arrays or make it a 2D array to contain more commands. That array could also be placed in a database.
Especially when there are many commands, with many variants, a database will be the better solution, because you can find the query word with one database query.
There's a variation I can think of that will also simplify the code when there are many commands. You could use an associative array to find the command:
$commandVariants = ['getdir' => 'getdir',
'dir' => 'getdir',
'directory' => 'getdir',
'getfolder' => 'getdir',
'getcontent' => 'getdir',
'content' => 'getdir',
'd-cont' => 'getdir',
'delete' => 'delete',
'del' => 'delete',
'remove' => 'delete',
'unlink' => 'delete'];
$queryWord = strtolower($queryWord);
if (isset($commandVariants[$queryWord])) {
$command = $commandVariants[$queryWord];
service_class::$command(<arguments>);
}
else echo "I don't recognize that command.";
This uses a variable identifier.
I have the following structure within a mongoDB collection:
{
"_id" : ObjectId("5301d337fa46346a048b4567"),
"delivery_attempts" : {
"0" : {
"live_feed_id" : 107,
"remaining_attempts" : 2,
"delivered" : false,
"determined_status" : null,
"date" : 1392628536
}
}
}
// > db.lead.find({}, {delivery_attempts:1}).pretty();
I'm trying to select any data from that collection where remaining_attempts are greater than 0 and a live_feed_id is equal to 107. Note that the "delivery_attempts" field is of a type hash.
I've tried using an addAnd within an elemMatch (not sure if this is the correct way to achieve this).
$qb = $this->dm->createQueryBuilder($this->getDocumentName());
$qb->expr()->field('delivery_attempts')
->elemMatch(
$qb->expr()
->field('remaining_attempts')->gt(0)
->addAnd($qb->expr()->field('live_feed_id')->equals(107))
);
I do appear to be getting the record detailed above. However, changing the greater than
test to 3
->field('remaining_attempts')->gt(3)
still returns the record (which is incorrect). Is there a way to achieve this?
EDIT: I've updated the delivery_attempts field type from a "Hash" to a "Collection". This shows the data being stored as an array rather than an object:
"delivery_attempts" : [
{
"live_feed_id" : 107,
"remaining_attempts" : 2,
"delivered" : false,
"determined_status" : null,
"date" : 1392648433
}
]
However, the original issue still applies.
You can use a dot notation to reference elements within a collection.
$qb->field('delivery_attempts.remaining_attempts')->gt(0)
->field('delivery_attempts.live_feed_id')->equals(107);
It works fine for me if I run the query on mongo.
db.testQ.find({"delivery_attempts.remaining_attempts" : {"$gt" : 0}, "delivery_attempts.live_feed_id" : 107}).pretty()
so it seems something wrong with your PHP query, I suggest running profiler to see which query is actually run against mongo
db.setProfilingLevel(2)
This will log all operation since you enable profiling. Then you can query the log to see which the actual queries
db.system.profile.find().pretty()
This might help you to find the culprit.
It sounds like your solved your first problem, which was using the Hash type mapping (instead for storing BSON objects, or associative arrays in PHP) instead of the Collection mapping (intended for real arrays); however, the query criteria in the answer you submitted still seems incorrect.
$qb->field('delivery_attempts.remaining_attempts')->gt(0)
->field('delivery_attempts.live_feed_id')->equals(107);
You said in your original question:
I'm trying to select any data from that collection where remaining_attempts are greater than 0 and a live_feed_id is equal to 107.
I assume you'd like that criteria to be satisfied by a single element within the delivery_attempts array. If that's correct, the criteria you specified above may match more than you expect, since delivery_attempts.remaining_attempts can refer to any element in the array, as can the live_feed_id criteria. You'll want to use $elemMatch to restrict the field criteria to a single array element.
I see you were using elemMatch() in your original question, but the syntax looked a bit odd. There should be no need to use addAnd() (i.e. an $and operator) unless you were attempting to apply two query operators to the same field name. Simply add extra field() calls to the same query expression you're using for the elemMatch() method. One example of this from ODM's test suite is QueryTest::testElemMatch(). You can also use the debug() method on the query to see the raw MongoDB query object created by ODM's query builder.
There is a comment on php.org at fetchAll page.
You might find yourself wanting to use FETCH_GROUP and FETCH_ASSOC at
the same time, to get your table's primary key as the array key:
// $stmt is some query like "SELECT rowid, username, comment"
$results = $stmt->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
// It does work, but not as you might expect:
$results = array(
1234 => array(0 => array('username' => 'abc', 'comment' => '[...]')),
1235 => array(0 => array('username' => 'def', 'comment' => '[...]')), );
but you can at least strip the useless numbered array out easily:
$results = array_map('reset', $results);
The code functions as expected and everything is accomplished with just line:
array_map('reset', $results);
After reading documentation array_map and reset function I don't really understand how the result is produced when both are combined in a single line.
Is it a safe solution? Would you recommend this kind of one-line or is it a side effect and should not be used, i.e. should I write a plain old loop to produce the same result?
Single line solution combining two standard functions is a very attractive solution to me. I just want to make sure there are no surprises.
The trick here is that reset in this very case is equal (suggest we are iterating through $results in array_map) to a $results[$i][0]. reset, besides internal pointer rewind, returns the first element of the passed array, as simple as that.
As a side note I would suggest achieve this behavior with PDO::FETCH_CLASS implementing ArrayAccess Interface.
Yes, it is safe. array_map() replaces an array member with callback function's result and reset() returns first member of array.
So,
array(0 => array('username' => 'abc', 'comment' => '[...]'))
is replaced with
array('username' => 'abc', 'comment' => '[...]')
as reset() returns this very line out of the former one.
But honestly, this is a kind of problem which never existed to me.
I don't understand what is so attractive in the idea of making one-liners using API functions only, producing whatever mind-breaking statements to abide one rule - "no user-defined functions at any cost".
I can make whatever function of my own, and it will be shorter than any of these code twists when called, yet have plain and readable syntax in body, without scratching your head both at writing and reading, without asking yourself a question "is it safe?".
So, a real one-liners would be my safe and convenient database access library's getInd family functions (which allows any field as a key, not primary index only):
$data = $db->getInd("id", "SELECT * FROM table WHERE category=?i", $cat);
$dic = $db->getIndCol("name", "SELECT name, id FROM cities");
you may notice that these are real one-liners, doing all the dirty and repetitive job inside, including query execution, parameter binding and stuff.
let's say I have a column in my database table that contains a list of comma-separated values like:
MyTable.values = a,b,c,d,e,f,etc....
How do I construct the condition in the find('all') function of cakePHP to retrieve the entries whose MyTable.values contain let's say "c" for example
Thanks
You should be able to use the LIKE operator. Percent-sign is a wildcard.
<?php
$this->Model->find("all", array(
"conditions" => array("Model.field LIKE" => "%c%")
));
?>
Gotta love that automagic.
Edit: Found it! The complex find conditions page has official documentation on this, though it's kind of buried.
I've been looking at questions like mongodb: insert if not exists , which gives pointers to "upsert" behavior.
However I expect only to create object if certain key is not found, i.e.
if ( $collection->findOne ( array ('key'=>'the_key') ) == NULL ) {
$collection->insert ( array ('key' => 'the_key', 'content' => 'the_content' );
} else {
// else don't touch it, so upsert would not fit.
}
I'm using PHP mongodb driver for this.
The above code is just the demonstration for my purpose. However there lacks the atomicity required. How this should be achieved?
Thanks in advance!
As you should define a unique index for this key anyway, you could also use it to prevent inserting objects with such a key multiple times. Have a look at the index documentation over at:
http://www.mongodb.org/display/DOCS/Indexes#Indexes-UniqueIndexes
Not a nice solution -- imo -- but the only one i can think of, if you do not want to do the update or can not do it for whatever reason. Keep in mind, that you will probably receive an error when trying to insert a object with this same key again, which you would have to handle.