I'm working with a Mongo database, and i have a Reading List with a BelongsToMany Relationship to a Stories table, it seems like this:
public function stories()
{
return $this->belongsToMany(Story::class, null);
}
Then, on the controller, i want to return a list of them, which contains only the id and title of the story list, i searched, and found many ways, but all them caused the same error, this is one of them:
$user = auth()->user();
$readingList = ReadingList::where('user_id', $user->id)->with(["stories"=> function ($query) {
$query->select("title");
}])->paginate(10);
And causes: Invalid argument supplied for foreach().
I took out the filter to verify it works, and answered correctly.
[
{
"_id": "6148dc2a23ef6d0e6838h123",
"name": "MyReadingList",
"stories": [
{
"_id": "611a64f5f735f32dcc5ab657",
"title": "Lorem",
"blurb": "Ipsum"
}
]
}
]
Am i doing this wrong, or how do you filter the fields with Jensseger library and BelongsToMany?
UPDATE
After a little debugging, seems like i found the cause, but i'm not sure how to solve it, in the Jensseger BelongsToMany file, there is a function like this:
protected function buildDictionary(Collection $results)
{
$foreign = $this->foreignPivotKey;
// First we will build a dictionary of child models keyed by the foreign key
// of the relation so that we will easily and quickly match them to their
// parents without having a possibly slow inner loops for every models.
$dictionary = [];
foreach ($results as $result) {
foreach ($result->$foreign as $item) {
$dictionary[$item][] = $result;
}
}
return $dictionary;
}
Here, $result is equal to {"_id":"6148dc2a23ef6d0e6838h123"}, which is my filtered result, but then it tries to access $result->$foreign, where $foreign is 'reading_list_ids', the pivot column, but is not here, hence returning a null and launching that error.
I tried adding reading_list_ids(And removing it from hidden) like $readingList = ReadingList::where('user_id', $user->id)->with("stories:title,reading_list_ids")->get();, still didn't work. Interestingly, i tried testing this without the filter, but with reading_list_ids hidden, it showed something like this {"_id":"6148dc2a23ef6d0e6838h123","title":"Lorem","blurb":"Ipsum","completed":"1"}, still not that field there, but it doesn't launch that error and works.
Does anyone knows how to bypass this, or make it work?
Related
I have 2 models, store and dvd with many to many relationship
dvd model:
public function stores() {
return $this->belongsToMany('App\store');
}
store model:
public function dvds ( ) {
return $this->belongsToMany('App\dvd');
}
Then, in controller, when I need fetch data from both models, I use code like this:
$res_store = store::orderBy("id","desc")->get();
foreach( $res_store as $row ) {
$dvds = $row->dvds()->get();
foreach ($dvds as $dvd) {
// echo columns here
}
}
This works, but my question is, I'm doing this in correct way? I'm asking because it seems 2 loops for this simple relation is somehow inefficient.
No, this is not the right way.
When looping over a $store, you can access the associated $dvd records via $store->dvds; there is no need to call $row->dvds()->get(), as that is executing a new query with the same result of $store->dvds. Full code should simply be:
$stores = Store::with("dvds")->orderBy("id", "DESC")->get();
foreach($stores AS $store){
foreach($store->dvds AS $dvd){
... // Do something with `$dvd`
}
}
The ::with("dvds") clause is known as "Eager loading", and prevents $store->dvds from needing execute another query behind the scenes.
Also, please name your models correctly. Classes in PHP are StudlyCase, so Store and DVD, not store and dvd.
I am looking for a solution which would withdraw essential data used to match other data from a different table. Then I would like to display this data inside a blade in a form of a table.
Inside the database, I have a "matching" entity which stores user's credentials which I would like to use for matching (for example desired price of the product). Entity contains "peopleID" as "matching" belongs to website users. When user is created, we assign matching options which are getting stored inside that "matching" entity. The number of rows inside Matching entity depends on the number of counties chosen during user creation stage.
I know that to withdraw matching data from the database I need to use a foreach loop.
The problem I have is when I output data inside the blade. For some reason it matches products only with the last item from an array. It should match prodtucts with all matching credentials.
Code:
$matchings = Match::where('PeopleID', '=', $id)->get();
$sales = DB::table('sales');
foreach ($matchings as $matching)
{
$county = $matching->county;
$sales->where('county', '=', $county);
}
$results = $sales->get();
So for one of the customers I have two matchings with different "counties". It only displays data for the last one added. How could I make it display data for other matching which contains a different county. I hope you know what I mean.
Thanks for any help.
Update - Major part of the code is done. Thank you for your help.
The second question is about adding the rest of matching options. As stated before the number of matches depends on the number of counties added. Each match has its own attributes. The idea is to show matched results for each county.
I know I will need some if statements to do this.
Here is an example which I would like to implement:
$maxprice = $match->maxprice;
$minprice = $match->minprice;
$brand_a = $match->brand_a;
$brand_b = $match->brand_b;
if($maxprice != '0')
{
$sales = DB::table('sales')->where('maxprice', '<=', $maxprice);
}
if($minprice != '0')
{
$sales = DB::table('sales')->where('minprice', '>=', $minprice);
}
if($brand_a == '1')
{
$sales = DB::table('sales')->where('brand_a', '1');
}
if($brand_b == '1')
{
$sales = DB::table('sales')->where('brand_b', '1');
}
To this code:
$user = User::find($id); // get our User with the Id (person id?)
$matches = $user->matches; // get all the matches
// or you could one line the above: $matches = User::find($id)->matches;
// get all the counties in the returned matches, could use pluck() method in higher version of laravel
$counties = [];
foreach($matches as $match) {
$counties[] = $match->county;
}
$results = DB::table('sales')->whereIn('county', $counties)->get();
Many Thanks for your help!
#update
Relationships:
Match:
public function people()
{
return $this->belongsTo('People', 'PeopleID', 'PeopleID');
}
People:
public function matches()
{
return $this->hasMany('Match', 'PeopleID', 'PeopleID');
}
I have a connection between those as Match holds people's "search" credentials. The first solution which you have provided works perfectly. Now, this solution filtered out sales by county which is a good move as now they need to be filtered by minimum and maximum price (minprice, maxprice) and other credentials such as brand_a and brand_b.
The idea of brand_a and brand_b:
Checkboxes are responsible for changing brand_a and brand_b value inside Matching. If these are checked the values inside Matching entity become '1'. If these are not checked they become '0' which means that sales don't have to be filtered out by those values.
Sales entity contains "Holdings" attribute. The value of "Holdings" can be brand_a or brand_b. Sales also contains "Price".
So, to make this clear:
Sale Entity contains: SaleID, Price, Holdings, County.
Holdings are values: brand_a or brand_b.
Price is just a number.
County is plain text.
Matching Entity contains: PeopleID, MinimumPrice, MaximumPrice, brand_a, brand_b, county.
PeopleID is a foreign key. We need to know which matching belongs to what user.
(there can be multiple matchings for one user depending on the number of counties chosen).
MinimumPrice and MaximumPrice are numbers.
brand_a and brand_b are the values (1 or 0) depending if the checkboxes were checked.
County is the name of a county.
Now, if person 1543 (peopleID = 1543) contains 3 matchings, each containing different search credentials.
1st:
PeopleID: 1543
MinimumPrice: 1000
MaximumPrice: 7000
brand_a: 0
brand_b: 1
county: county_a
2nd:
PeopleID: 1543
MinimumPrice: 2500
MaximumPrice: 10000
brand_a: 1
brand_b: 1
county: county_f
3rd:
PeopleID: 1543
MiniumPrice: 2000
MaximumPrice:9500
brand_a: 0
brand_b: 0
county: county_d
I need to match this data against the data that is inside the Sales. There can be over a 1,000 different sales with different prices etc. I just need to filter them and display Sales that are desired by the person based on person's matching.
I hope this better presents you the situation. Thanks.
In short, I belive you need to leverage Eloquent Relationships to easily retrieve the data you desire. Read up on relationships in the docs here: https://laravel.com/docs/4.2/eloquent#relationships.
I've made some assumptions so you may need to work the following into your actual setup. Also, I found it quite difficult to 100% understand your DB structure from your question, but from what I gather from your question your DB structure is like this:
User/Person *has many* Match
(Note: Name may be wrong, but you didn't mention what it's called in the question all I can see is the word "user" and "personId")
Match *belongs to* User/Person
Based on this I think you should set up your relationships like this:
User
class User extends Eloquent {
public function matches()
{
return $this->hasMany('Match');
}
//...
}
Match
class Match extends Eloquent {
public function user()
{
return $this->belongsTo('User');
}
//...
}
Then your code can look like this:
$user = User::find($id); // get our User with the Id (person id?)
$matches = $user->matches; // get all the matches
// or you could one line the above: $matches = User::find($id)->matches;
// get all the counties in the returned matches, could use pluck() method in higher version of laravel
$counties = [];
foreach($matches as $match) {
$counties[] = $match->county;
}
$results = DB::table('sales')->whereIn('county', $counties)->get();
A better approach to this issue (I think) would be to give County it's own entity then Match would have a county_id then you can use a has many through relationship if you can link up Match, County and Sales. You can read more about has many through in the docs here: https://laravel.com/docs/4.2/eloquent#has-many-through
Also, a side point... this part of your code:
$sales->where('county', '=', $county);
will just continuously add where statements to your query which I would imagine won't return anything if there's more than one.
Just to make this clearer, imagine you have 2 counties "county_1" and county "county_2", through your for loop your query would end up like this:
WHERE COUNTY = "county_1" // first loop
AND COUNTY = "county_2" // second loop
and as you can see a match cannot be two counties at one time! So you were probably looking for ->orWhere('county', 'county_value') after the first one was added, but a better approach is to use whereIn('county', $countiesArray) which you can pass an array you've built up, which is what I've done above.
Hope this helps! Let me know if it wasn't clear.
Edit
The best approach would be to establish relationships between the Sale and Match entities. As I still don't fully understand your database schema I can't advise so well on how you would approach that. If you gave some more details it may be possible.
Alternatively, you could approach the code by building up an array which you will use for applying conditions to your query. Consider your updated question with the four if statements, anytime you're repeating yourself like that, more often that not it can be simplified.
$filters = [
'maxprice' => $match->maxprice,
'minprice' => $match->minprice,
'brand_a' => $match->brand_a,
'brand_b' => $match->brand_b,
];
$salesQuery = DB::table('sales');
foreach($filters as $key => $value) {
if($value) {
$salesQuery->where($key, $value);
}
}
$results = $salesQuery->get();
As your conditionals are a bit stricter in your code from your question, you do to it like this instead:
foreach($filters as $key => $value) {
if ($value) {
if (in_array(['brand_a', 'brand_b'], $key)) {
$salesQuery->where($key, $value);
} else if($key === 'minprice') {
$salesQuery->where($key, '>=' $value);
} else if($key === 'maxprice') {
$salesQuery->where($key, '<=' $value);
}
}
}
the good thing about this approach is that you can easily add new conditionals via the filters array without having to write a new if statement and query/where code each time.
I'll stress this probably isn't the best approach, ideally you'd leverage Eloquent Relationships, but it may be a starting point if you can't figure that out right away.
I am using Propel 2. I am hydrating objects through the relations, like so:
$return = OrderQuery::create()
->joinWith('Customer')
->joinWith('Status')
->find()
->toArray(TableMap::TYPE_PHPNAME, true, [], true);
The resulting Array would look something like this:
{
"Id": 1,
"CustomerId": 1,
"StatusId": 1,
"Initiated": "2016-01-01T01:01:01+00:00",
"Customer": {
"Id": 1,
"Forname": "Test",
"Surname": "Smith",
"Orders": [
"*RECURSION*"
]
}
"Status": {
"Id": 1,
"Title": "title 1",
"Priority": 1,
"Orders": [
"*RECURSION*"
]
},
}
I want to remove the fields where the value is *RECURSION*. I tried using the $alreadyDumpedObjects (3rd) parameter to toArray() but that didn't seem to help. I could also do some form of array walking with unset() calls, but I'm hoping there's a better way, maybe with a formatter or something?
For bonus points, I'd quite like to remove the columns which define the foreign key relationship. For instance, CustomerId would go, but Customer would remain.
Note for brevity: This answer has some really helpful information, but there is a solution, despite this answer saying there isn't.
The RECURSION string is pretty much hardcoded in propel (in src/Propel/Generator/Builder/Orm/ObjectBuilder.php):
if (isset(\$alreadyDumpedObjects['$objectClassName'][\$this->hashCode()])) {
return '*RECURSION*';
}
I suppose you could override the object builder, but I doubt that's what you are looking for. Thus, the only other viable way is what you did not want to do, looping over the array and using unset. Something like this:
$array = StudyQuery::create()
->leftJoinWithInstitute()
->find()
->toArray();
var_dump($array);
echo PHP_EOL . PHP_EOL . PHP_EOL;
var_dump(cleanupData($array));
/**
* #param array $array
*
* #return array
*/
function cleanupData(array $array)
{
$relationsFound = [];
foreach ($array as $key => $item) {
if ($item === ["*RECURSION*"] || $item == "*RECURSION*") {
unset($array[$key]);
} elseif (is_array($item)) {
$array[$key] = cleanupData($item);
$relationsFound[] = $key;
}
}
foreach ($relationsFound as $relation) {
$key = $relation . 'Id';
if (isset($array[$key])) {
unset($array[$key]);
}
}
return $array;
}
This should also filter out the ***Id-fields, if that relation is present (provided the PHPName for the relation matches the column name).
When you call toArray method on a Propel ObjectCollection it will call the toArray on every item of the collection (the result of the query).
So there are two way to achieve what you're trying to do:
1) Overwrite the toArray method in the Customer and Status model classes or in the CustomerCollection and StatusCollection ones (a quick&dirty solution since it gets applied every time you use the toArray method...).
2) Extend the ArrayFormatter class to define a "skip Customer and Status strategy" in the format method and then add it as the formatter to use (with the setFormatter method on the collection) only when you need this particular behavior.
Some refs:
http://propelorm.org/documentation/reference/model-criteria.html#using-an-alternative-collection-class
http://propelorm.org/documentation/reference/model-criteria.html#using-an-alternative-formatter
The answer, it seems, appears to be very simple.
If I use a standard ArrayFormatter like this:
$return = OrderQuery::create()
->setFormatter('Propel\Runtime\Formatter\ArrayFormatter');
->joinWith('Customer')
->joinWith('Status')
->find()
->toArray();
What is returned is an ArrayCollection that, when toArray() is called on, is exactly the same as my original output apart from the recursion fields are missing.
Note, when getting one result, for instance with ->findPk(1), it will return an associative array, so you shouldn't use ->toArray() explicitly.
Did you try calling:
unset($return['Customer']['Orders']);
unset($return['Status']['Orders']);
The "in" property used in Extbase does not seem to be working for me.
$actor contains an array of Actor model objects. My Movie model and Actor are in m:n relation.
I tried something like this in my Movie Repository:
$query=$this->createQuery();
$query->matching($query->in('actors',$actors));
$result = $query->execute()->toArray();
$result is showing NULL
I tried passing array of actor uids too but that wont work as well:
$query->matching($query->in('actors',[$actor_1_uid,$actor_2_uid]));
There is of course contains but using in should be more convenient.
I don't see any problem in your statement. Just to be clear, a "in" statement must be placed somewhere inside a matching statement, which is correct in your case.
However, you should change your create query for
$query = $this->createQuery();
instead of
$query=$this->create->query();
If you have still no result, I suggest you check the exact the SQL statement executed by extbase, there's a tricky way to do it in TYPO3.
You have to locate the following file in the core:/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
locate the replacePlaceHolders function and add the following codes at the end of the function:
if (strpos( $sqlString, "my_table_name" ) !== false) {
echo $sqlString;
}
I will echo every statement that is being made for the following "my_table_name" table. Of course, never do that in your production server.
I hope it will help!
Cheers,
Olivier
Sorry, but $query->in is the wrong approach. AFAIK it will not work for m:n reations, only for 1:n.
Try something like this, $actors being a query result from the model or the actors repository:
$constraints = array();
foreach ($actors as $actor) {
$constraints[] = $query->contains('actors', $actor->getUid());
}
if (!empty($constraints)) {
$result = $query->matching($query->logicalOr($constraints))->execute();
}
Of course you can use your own array of uids for the loop, then just drop the getUid() method
I'm trying to make what should be a very simple "list all" function using Propel ORM - for Backbone.js to read. This is what I want to do, and in my opinion, should work:
$users = UsersQuery::create()
->find();
echo $users->toJSON();
However, when I'm running that, the results I'm getting are:
{"Users_0":{"Id":1,"EmailAddress":"sdf","Password":"sdf","CreatedAt":null,"ModifiedAt":null},
"Users_1":{"Id":2,"EmailAddress":"dsf","Password":"sdf","CreatedAt":null,"ModifiedAt":null}}
Whilst it's valid JSON, the fact that ever row is an array in the main array is throwing off my JSON. What I need it to return is JSON like this:
[{"Id":1,"EmailAddress":"sdf","Password":"sdf","CreatedAt":null,"ModifiedAt":null},{"Id":2,"EmailAddress":"dsf","Password":"sdf","CreatedAt":null,"ModifiedAt":null}]
I've created the below function (as a test) and it works perfectly, but surely Propel (or Slim, the framework I'm using) has way of stopping everything being inside an array? Here the hack;
$users = UsersQuery::create()
->find();
$json = '[';
foreach($users as $user){
$json = $json.$user->exportTo('JSON').',';
}
$json = $json.']';
echo str_replace("},]", "}]", $json);
Any help would be greatly appreciated! Thanks all.
I hate to say it, but I think this is just one of those "that's how Propel works" situations. That said, you could improve your helper function a little to be more robust.
I would put this code in your UserQuery class:
class UsersQuery extends BaseUsersQuery {
...
public function toJSONArray() {
$users = $this->find();
$userArray = array();
foreach($users as $user){
array_push($userArray, $user->toArray());
}
return json_encode($userArray);
}
}
And then use it like so...
$userJSON = UsersQuery::create()->toJSONArray();
Or if you have other criteria...
$userJSON = UsersQuery::create()
->filterBySomeField("someValue")
// other Criteria ...
->toJSONArray();
is there a possible solution to use this and a select filter in one statement. Something like this:
$ojson = TblproductQuery::create()
->select(array('ProdtID', 'DivnID'))
->toJsonArray();
One thing the accepted answer does not address is when you have an object with a nested collection. Like maybe you have a Bunch of tests with answers like so:
[
{
id:test1,
answers : [
{ id: 1, answer: pig},
{ id: 2, answer: dog}
]
},
{
id:test2,
answers : [
{ id: 5, answer: duck},
{ id: 6, answer: swan}
]
}
]
The above won't play nicely with backbone collections when you try to use the accepted answer. This is because every propel model will call the PropelCollection::toArray() method on any propel collections within itself
The PopelCollection::toArray() method will only return itself as an associative array in php which gets converted to an unsorted set in JSON rather than an array. Backbone collections are sorted (arrays) only.
To fix this, I just changed the toArray() method in the propel source file PropelCollection.phpto the following:
public function toArray(
$keyColumn = null,
$usePrefix = false,
$keyType = BasePeer::TYPE_PHPNAME,
$includeLazyLoadColumns = true,
$alreadyDumpedObjects = array()){
$ret = array();
foreach ($this as $key => $obj) {
array_push($ret, $obj->toArray($keyType, $includeLazyLoadColumns,
$alreadyDumpedObjects, true);
}
return $ret;
}
I haven't seen how this affects the toXML or toYAML methods, but it allows the toJSON method to work as I want with nested collections like my example above.