Background
I have an internationalized DB that stores its strings for different languages like this:
products
id
price
product_translation
id
product_id
language_id
name
description
languages
id
name (e.g. 'English', 'German')
code (e.g. 'en', 'de')
With appropriate Models for each table (Product, ProductTranslation, Language). In my views I want to fetch a list of products like this:
// get first 20 products, list name and price.
#foreach(Product::take(20)->get() as $product)
{{$product->translations->name}} {{$product->price}}
#endforeach
Problem
My app will return product names according to what the current App::getLocale() is set to (i.e. en and de).
I'm just starting out with Laravel's Eloquent, I'm unsure how to specify the correct relationships (or if I'm actually doing it correctly at all).
My attempt
I have specified a OneToMany relationship in between Product and ProductTranslation:
class Product extends \Eloquent {
protected $with = ['translations'];
public function translations()
{
return $this->hasMany('ProductTranslation');
}
}
This works fine but will return all the translations (we only want the current locale).
I then specify a OneToOne relationship between ProductTranslation and Language:
class ProductTranslation extends \Eloquent {
protected $with = ['language'];
public function language()
{
return $this->hasOne('Language')
->where('code', App::getLocale());
}
}
I know this doesn't work and I am stumped at what to do next. Does anyone have a cleaner approach?
class ProductTranslation extends \Eloquent {
protected $with = ['language'];
public function language()
{
return $this->hasOne('Language');
}
}
In route or controller
ProductTranslation::language()->where('code', '=', App::getLocale())->get();
To keep this in the model do this
public static function getLocale()
{
return static::language()->where('code', '=', App::getLocale())->get();;
}
Call the function using ProductTranslation::getLocale()
Laravel has built-in system for translations and with a little work you can make it work with the database, however that is probably not what it was designed for.
You can't fetch the ones you want cause relationships are based on ids (foreign keys) and not for string constraints or similar.
In your view you could look into filtering out the ones that are not for the language code you wanted using filter(): http://laravel.com/docs/4.2/eloquent#collections
Or You could consider moving the translations to the proper place as hard-coded: http://laravel.com/docs/4.2/localization and if that is not possible you could look into fetching the translations from database still using the method described by that link. Eventually it just returns the array of the translations and does not care how did you build the array, hard-coded or from database.
Related
I'm trying to use a HasMany relation in a HasOne.
I have following Models:
class Auction extends Model
{
//...
public function bids(): HasMany
{
return $this->hasMany(Bid::class, 'auction_id');
}
public function approvedBids(): HasMany
{
return $this->bids()->approved();
}
public function topBids(): HasMany
{
return $this->approvedBids()->orderByDesc('price')->take(10);
}
public function topBid(): HasOne
{
//return $this->topBids()->firstOfMany(); // Not Working
//return $this->hasOne(Bid:class, 'auction_id)->ofMany('price','max')->approved(); // not working
//return $this->hasOne(Bid:class, 'auction_id)->approved()->ofMany('price','max'); // not working
//return $this->hasOne(Bid::class, 'auction_id')->ofMany('price', 'max'); // working but not as I expecting
}
}
class Bid extends Model
{
//...
public function scopeApproved(Builder $query): Builder
{
return $query->where('state', BidState::STATE_APPROVED);
}
//...
}
As you can see in the source, I'm looking for a way to make a relation that retrieve the Top Bid (ONE BID) from topBids() relation, but I don't know how, and none of my approaches works:
$this->topBids()->firstOfMany(); // Not Working
$this->hasOne(Bid:class, 'auction_id')->ofMany('price','max')->approved(); // not working
$this->hasOne(Bid:class, 'auction_id')->approved()->ofMany('price','max'); // not working
Unfortunately these shouldn't be a relationships
Real question is why are you trying to make these relationships?
Usually you should be using relationships on model to describe how they are correlating together within the database, the rest of the things you should be defining as a scope on a query or a model, or as an attribute.
So, what I'm trying to say is this:
Keep bids as a relationship, as that is actually a relationship to the Bid model
Update approvedBids to be a scope (or an attribute)
Update topBids to be a scope (or an attribute)
Then, you will be able to find top bid easily by doing something like this:
$this->topBids->first() -> if it is an attribute
$this->topBids()->first() -> if it is a scope
This is how you can create a scope: https://laravel.com/docs/9.x/eloquent#local-scopes
In the end, you can even create an attribute that will allow you to retrieve topBid like this:
public function getTopBidAttribute(){
$this->bids()->approved()->orderByDesc('offered_token_price')->first();
}
Then later you can just do $this->topBid.
I think I've found the solution
public function topBid(): HasOne
{
return $this->hasOne(Bid::class, 'auction_id')
->approved()
->orderByDesc('price');
}
You see the problem was in ofMany() function, which creates a huge SQL and I don't know why!
I've returned a HasOne object here, which supports all kinds of query manipulations. Basically HasOne class, tells the main query, to:
Retrieve the first record of the query I've provided.
So if we use orderBy it only provides an order for HasOne's query. and the main query will take cares of the rest and selects the first record.
My business can have many pages and here i am defining my model:
class PageList extends Model
{
public function business()
{
return $this->hasMany('App\Business','business_id');
}
}
My Controller
public function pageListHere()
{
$list = PageList::all();
return view('page-list',compact('list'));
}
and my view having for each loop
<td>{{optional($value->business)->business_url}}.spikesales.io/{{$value->page_url}}</td>
In my relation ship if i do hasOne('App\Business','business_id'); it works fine but its only display one page against business but i have multiple pages in my database and one business can have many pages and if write hasMany('App\Business','business_id'); it show me the error Property [business_url] does not exist on this collection instance
I want to show the multiple pages against one business.
your help will be highly appreciated!
I think you are a little bit confused. In your model, you are actually defining the relation A page has many businesses, and you are telling that the businesses table has a column business_id, which Laravel tried to find but couldn't, resulting in an error.
The hasMany function works in this way:
// In the Post Model...
public function comments() {
return $this->hasMany('App\Comment', 'post_number');
}
This makes the relation A Post has many Comments and makes Laravel assume that the comments table has post_number column.
You want to show multiple pages against one business. So, A business has many Pages. To code it...
// In your BUSINESS model...
public function pages() {
// Notice that $this refers to the Business Model
return $this->hasMany('App\PageList', 'business_id');
}
You can optionally (but I think you should) omit the 'business_id' part. It will safely be assumed by default.
And the inverse of the relation, A Page belongs to A business, will be coded as...
// In your PAGELIST model...
public function business() {
return $this->belongsTo('App\Business');
}
You need to loop/iterate over the business relationship since the result will return in a array of collection:
#foreach($lists as $list)
// others markup
#foreach($list->business as $business)
// do something here
{{ $business->business_url }}
#endforeach
#endforeach
I have one table in the database(mysql). But this table stores several slightly different types of rows. The type depends on this tables's type column. I have an abstract ActiveRecord class for a table and several descendant subclasses implementing slightly different logic for the rows of the same table of different types. Now I am implementing an update controller action for all the types of rows. I am provided with an id of the row and need to create an ActiveRecord instance representing the row with this id. But I somehow need to create instances of different subclasses depending on the type of the corresponding row.
If I were provided with both a type and an id I could've used a factory to pick a corresponding subclass. But I can already have the type in the database and an id gives me enough information to pick it from there. But if I were to pick the type from the database first and then to create an instance of the corresponding subclass that would've meant executing the same query twice.
I want to find a good way to get the data from the database and then pick a right ActiveRecord subclass to create an instance for it without making excessive queries or requiring excessive data. Is there a way to do it Yii2?
Or should I approach this problem somehow differently? The actual problem is having several almost the same but a bit different entities stored in a single table with a bit different business-logic.
One of approaches to this problem is called "Single table inheritance" and described by Martin Fowler here. There is also good article about its implementation in Yii 2 in samdark's (one of the main Yii 2 contributors) cookbook, which is currently in process of writing but is available on Github.
I'm not going to copy the whole article, but leaving just link is also not enough. Here are some important things:
1) Create common query for all types of objects (for example cars):
namespace app\models;
use yii\db\ActiveQuery;
class CarQuery extends ActiveQuery {
public $type;
public function prepare($builder)
{
if ($this->type !== null) {
$this->andWhere(['type' => $this->type]);
}
return parent::prepare($builder);
}
}
2) Create separate model for each type (extending from common model Car):
Sport cars:
namespace app\models;
class SportCar extends Car
{
const TYPE = 'sport';
public static function find()
{
return new CarQuery(get_called_class(), ['type' => self::TYPE]);
}
public function beforeSave($insert)
{
$this->type = self::TYPE;
return parent::beforeSave($insert);
}
}
Heavy cars:
namespace app\models;
class HeavyCar extends Car
{
const TYPE = 'heavy';
public static function find()
{
return new CarQuery(get_called_class(), ['type' => self::TYPE]);
}
public function beforeSave($insert)
{
$this->type = self::TYPE;
return parent::beforeSave($insert);
}
}
3) Override instantiate() method in Car model to return correct type of cars:
public static function instantiate($row)
{
switch ($row['type']) {
case SportCar::TYPE:
return new SportCar();
case HeavyCar::TYPE:
return new HeavyCar();
default:
return new self;
}
}
Then you can use any type of cars individually as regular models.
I have two models in my Laravel 4.2 web application, User and Group. A user can be a member of many groups, and a group can have many members. Both models are thus joined with a many-to-many relationship:
<?php
class User extends Eloquent {
public function groups()
{
return $this->belongsToMany('Group');
}
}
class Group extends Eloquent {
public function users()
{
return $this->belongsToMany('User');
}
}
?>
One of my API resources is /groups, which lists all groups available within the app:
<?php
$groups = Group::with('users')->all();
?>
This works, however in the JSON response each user contains all fields from the users table (excluding of course those in the $hidden attribute). I would like this relationship to return only a specific set of fields instead of the whole table.
In other relationship types I can easily achieve this with the following statement (assume now that users may belong to only one group):
<?php
public function users()
{
return $this->hasMany('User')->select(['id', 'first_name', 'last_name']);
}
?>
However the above does not seem to work with many-to-many relationships. I came across this question which apparently refers to the same issue and it looks like this was not possible in Laravel 4.1. The author of the chosen answer, tptcat, provides a link to an issue on Laravel's Github issue tracker, but the link is no longer working and I couldn't figure whether this issue is still open in 4.2.
Has anybody come across this and successfully managed to solve it?
{
return $this->belongsToMany('User')->select(array('id', 'name'));
}
use this
The all method takes in an array of column names as a parameter.
If you look at the source, it takes * (which means everything) by default.
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Database/Eloquent/Model.php#L624-L629
You can pass in the columns that you needed and it should return the results only with the specified columns.
<?php
$groups = Group::with('users')->all(array('first_column', 'third_column'));
Use like this.
<?php
class User extends Eloquent {
public function groups()
{
return $this->belongsToMany('Group')->select(array('id', 'name'));
}
}
class Group extends Eloquent {
public function users()
{
return $this->belongsToMany('User')->select(array('id', 'name'));
}
}
?>
Instead of selecting column in relationship, you can select column as below:
$groups = Group::with('users:id,first_name,last_name')->all();
And when you are selecting column in relationship, make sure that you are selected foreign key of relation table
Okay so I'm building a pretty large application in Laravel. Users manage their own virtual soccer teams. I have a users table then I have a teams table with team specefic things like name, level,and arena, etc. For the arena though I decided to add a arenas table and then add a arena_id column in the teams table instead of just adding the arena name to the teams table.
so here is the basic relantionship:
User hasOne Team
Team hasOne User
Team hasOne Arena
Arena hasOne Team
so if I wanted to get the arena for a user I call the method
$user = User::with('team')->where('username', '=', $username)->first();
$user->team->arena->arena_name;
and everything works fine; however I feel there is a much cleaner or simpler way of doing this. Is there or is this fine for the aplication?
There is nothing wrong with the way you are doing it. That is a perfectly good way of doing it for your needs. However something that might help is creating a getArenaFromUsername() method in the User model. Your User model would look something like this:
<?php
class User extends \Eloquent {
protected $fillable = [];
public function getArenaFromUsername($username)
{
$user = User::with('team')->where('username', '=', $username)->first();
return $user->team->arena->arena_name;
}
}
So then to get the arena name from a controller you just do:
$user = new User;
$arena = $user->getArenaFromUsername($username);
-----------------------------------------OR-----------------------------------------------
Or use dependency injection by doing the following in your controller using the same method we just created in the model:
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
then to use it you can use one line in any method in your controller like so:
$this->user->getArenaFromUsername($username);
These are all different ways of abstracting your query to make it more reusable and cleaner to call in your controller. Don't be afraid to make public methods in your model to call.
A couple things.
You can eager load the sub-relationship like so:
User::with('team', 'team.arena')...
You can also create an accessor function (http://laravel.com/docs/eloquent#accessors-and-mutators) on your User model to make it a first-class property on the User object:
// accessed via $user->arena
public function getArenaAttribute {
return $this->team->arena;
}
// accessed via $user->arenaName
public function getArenaNameAttribute {
return $this->team->arena->arena_name;
}