I have this database that I got from this post that manages products and its variants:
+---------------+ +---------------+
| PRODUCTS |-----< PRODUCT_SKUS |
+---------------+ +---------------+
| #product_id | | #product_id |
| product_name | | #sku_id |
+---------------+ | sku |
| | price |
| +---------------+
| |
+-------^-------+ +------^------+
| OPTIONS |------< SKU_VALUES |
+---------------+ +-------------+
| #product_id | | #product_id |
| #option_id | | #sku_id |
| option_name | | #option_id |
+---------------+ | value_id |
| +------v------+
+-------^-------+ |
| OPTION_VALUES |-------------+
+---------------+
| #product_id |
| #option_id |
| #value_id |
| value_name |
+---------------+
The problem is, that I don't know how would I get the SKU at the moment that a user selects the options of the product he wants:
SKU_VALUES
==========
product_id sku_id option_id value_id
---------- ------ --------- --------
1 1 1 1 (W1SSCW; Size; Small)
1 1 2 1 (W1SSCW; Color; White)
1 2 1 1 (W1SSCB; Size; Small)
1 2 2 2 (W1SSCB; Color; Black)
Let's suppose that the user selects the product with ID 1 and the options size-small and color-black, how am I able to get the sku_id (in this case I would want value 2 from sku_id) in order to get the price that's inside the PRODUCT_SKUS table.
I cannot do something like this for obvious reasons:
SELECT sku_id FROM SKU_VALUES
WHERE (SKU_VALUES.option_id = 1 AND SKU_VALUES.value_id = 1)
AND (SKU_VALUES.option_id = 2 AND SKU_VALUES.value_id = 2)
NOTE that it seems that I would need to append the same number of conditions (or whatever I need) as the number of options that are available from a product, in this case there are just 2 rows because the product has 2 options (size and color), but the product may have "n" options.
I would appreciate if someone could guide me for this query and if it's possible doing it with Laravel Eloquent instead of using RAW query.
The models I have created are the following:
"Product" Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Producto extends Model
{
protected $table = 'productos';
protected $fillable = [
'nombre',
'descripcion'
];
public function opciones(){
return $this->hasMany('App\Models\OpcionProducto', 'producto_id');
}
public function skus(){
return $this->hasMany('App\Models\ProductoSku', 'producto_id');
}
}
"Options" Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCompositePrimaryKey;
class OpcionProducto extends Model
{
use HasCompositePrimaryKey;
protected $table = 'productos_opciones';
protected $primaryKey = array('producto_id', 'opcion_id');
protected $fillable = [
'producto_id',
'opcion_id',
'nombre_opcion',
'valor'
];
public function producto(){
return $this->belongsTo('App\Models\Producto', 'producto_id');
}
public function valores(){
return $this->hasMany('App\Models\OpcionValorProducto', 'opcion_id', 'opcion_id');
}
public function skusValores(){
return $this->hasMany('App\Models\SkuValor', 'opcion_id', 'opcion_id');
}
}
"OptionValues" Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCompositePrimaryKey;
class OpcionValorProducto extends Model
{
use HasCompositePrimaryKey;
protected $primaryKey = array('producto_id', 'opcion_id', 'valor_id');
protected $table = 'productos_opciones_valores';
protected $fillable = [
'producto_id',
'opcion_id',
'valor_id',
'valor_variacion',
'valor'
];
public function producto(){
return $this->belongsTo('App\Models\Producto', 'producto_id');
}
public function opcion(){
return $this->belongsTo('App\Models\OpcionProducto', 'opcion_id', 'opcion_id');
}
}
"Product_SKUS" model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCompositePrimaryKey;
class ProductoSku extends Model
{
use HasCompositePrimaryKey;
protected $primaryKey = array('producto_id', 'sku_id');
protected $table = 'productos_skus';
protected $fillable = [
'producto_id',
'sku_id',
'imagen_id',
'precio',
'stock',
'sku'
];
public function producto(){
return $this->belongsTo('App\Models\Producto', 'producto_id');
}
public function valoresSku(){
return $this->hasMany('App\Models\SkuValor', 'sku_id');
}
}
}
"SKU_VALUES" model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCompositePrimaryKey;
class SkuValor extends Model
{
use HasCompositePrimaryKey;
protected $primaryKey = array('producto_id', 'sku_id', 'opcion_id');
protected $table = 'valores_skus';
protected $fillable = [
'producto_id',
'sku_id',
'opcion_id',
'valor_id',
];
public function producto(){
return $this->belongsTo('App\Models\Producto', 'producto_id');
}
public function opcion(){
return $this->belongsTo('App\Models\OpcionProducto', 'opcion_id', 'opcion_id');
}
public function sku(){
return $this->belongsTo('App\Models\ProductoSku', 'sku_id', 'sku_id');
}
}
After going through your question, this is the code I came up with. Of course this is un-tested. Please give this a shot.
$skuValor = SkuValor::with('producto', 'opcion.valores', 'sku')
->whereHas('producto', function ($q) use ($request) {
$q->where('id', $request->get('product_id')); // id: 1
})
->whereHas('opcion', function ($q) use ($request) {
$q->whereIn('id', $request->get('option_ids')) // id: [1, 2] where 1=size, 2=color
->whereHas('valores', function($q2) use ($request) {
$q2->whereIn('id', $request->get('value_ids')); // id: [1, 3] where 1=small, 3=pink
});
})
->get();
$skuValor->sku->id; // sky id
with() : This is called Eager Loading. Load some relationships when retrieving a model.
whereHas() : Querying Relationship Existence. This method allow you to add customized constraints to a relationship constraint.
use() : Passing data so that inner (that particular) query can use it. Notice we have used 1 for opcion and another one for valores.
whereIn() : This method verifies that a given column's value is contained within the given array.
Comment if you get stuck.
Here is a solution using pure SQL.
This is your attempt using a raw query:
select sku_id
from sku_values
where (option_id = 1 and value_id = 1) and (option_id = 2 and value_id = 2)
This doesn't work because you need to search across rows sharing the same sku_id rather than on each row. This suggest aggregation:
select sku_id
from sku_values
where (option_id, value_id) in ((1, 1), (2, 2)) -- either one combination or the other
group by sku_id
having count(*) = 2 -- both match
You can easily extend the query for more options by adding more combinations in the where clause predicate and incrementing the target count in the having clause accordingly. For example, this filters on 4 criterias:
select sku_id
from sku_values
where (option_id, value_id) in ((1, 1), (2, 2), (3, 10) (12, 17))
group by sku_id
having count(*) = 4
It is also possible to filter by option names and values by adding more joins in the subquery:
select sv.sku_id
from sku_values sv
inner join options o
on o.product_id = sv.product_id
and o.option_id = sv.option_id
inner join option_values ov
on ov.product_id = sv.product_id
and ov.option_id = sv.option_id
and ov.value_id = sv.value_id
where (o.option_name, ov.value_name) in (('Size', 'Small'), ('Color', 'Black'))
group by sv.sku_id
having count(*) = 2
Now, say you want to get the corresponding product name and price: you can join the above query with the relevant tables.
select p.product_name, ps.price
from products p
inner join product_skus ps
on ps.product_id = p.product_id
inner join (
select sv.sku_id
from sku_values sv
inner join options o
on o.product_id = sv.product_id
and o.option_id = sv.option_id
inner join option_values ov
on ov.product_id = sv.product_id
and ov.option_id = sv.option_id
and ov.value_id = sv.value_id
where (o.option_name, ov.value_name) in (('Size', 'Small'), ('Color', 'Black'))
group by sv.sku_id
having count(*) = 2
) x
on x.sku_id = ps.sku_id
This is possible using Laravel subqueries but I would suggests the use create a RAW query to get those values you need and then implement it with Laravel Query builder.
For queries that involves many tables Eloquent can result some kind of unefficient and it will take you to write unmaintainable code.
Extra suggestion, even when you your table names are in other languages it is recomendable create full english models, you can specify what field is related to what property.
animals:
| id | name |
|----|------|
| 1 | cat |
| 2 | dog |
| 3 | frog |
category:
| id | name |
|----|--------|
| 1 | green |
| 2 | blue |
| 3 | orange |
animals_category:
| animals_id | category_id |
|------------|-------------|
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
What I want to do is get the categories for dog:
green, blue
This is my approach:
Controller:
$id = '2';
$result = $this->getDoctrine()->getRepository('Animals:Category')->findByIdJoinedToCategory(['id'=>$id]);
Animals Repository:
public function findByIdJoinedToCategory()
{
$query = $this->getEntityManager()
->createQuery(
'SELECT a, b FROM Animals:Category a
JOIN a.category b');
try {
return $query->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
But I get an error message:
Unknown Entity namespace alias 'Animals'.
entity Animals:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="App\Repository\AnimalsRepository")
*/
class Animals
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Category")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
private $category;
public function getId(): ?int
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getCategory()
{
return $this->category;
}
public function setCategory($category): self
{
$this->category = $category;
return $this;
}
public function addCategory(Category $category): self
{
$this->category[] = $category;
return $this;
}
public function __construct()
{
$this->category = new ArrayCollection();
}
}
There's no Animals:Category entity. You have entities Animals and Category.
The correct answer depends if you're using Symfony 3 or 4, because Symfony 3 uses entity aliases (namespacing with : notation which you're trying ot use), while Symfony 4 prefers full qualified namespace (\App\Entity\Animals).
So, first mistake is in line where you're trying to get repository:
getRepository('Animals:Category')
And the second in findByIdJoinedToCategory() in DQL query :
'SELECT a, b FROM Animals:Category a
JOIN a.category b'
Now solutions:
Symfony 3
Since it looks you don't have any bundles (I guess it's Symfony 4 but whatever), you don't have any entity namespace alias, so you should simply use its name.
getRepository('Animals')
Now, I assume, that with a you want to reference Animals entity/table, so it should be
'SELECT a, b FROM Animals a
JOIN a.category b'
Symfony 4
If you use Symfony 4, then use should use entity FQNS as entity name (App\Entity\Animals).
So it would be
getRepository('\App\Entity\Animals')
or
getRepository(\App\Entity\Animals::class)
to get repository. The second one is better, because it will be easier to refactor when needed (IDE will be able to find usage of class).
And in query it would be
'SELECT a, b FROM App\Entity\Animals a
JOIN a.category b'
or if you would like to avoid using hardcoded string class names:
'SELECT a, b FROM ' . \App\Entity\Animals:class . ' a
JOIN a.category b'
I have two entities Modules and Orders where One order have Many modules and I'm wondering how to fetch an array collection of modules persisted as follow:
Table: Orders
id | modules | user_id | ... | created_at |
----------------------------------------------------
1 | [2,6,5] | 12 | ... | 2018-07-28 00:00:00 |
----------------------------------------------------
As you can see my modules are persisted as array. So after that how can I make Doctrine (with Symfony) to get my modules
I think you need a ManyToOne relationShip ... as I know, we never store an array in database.
in your example order can have many modules and module can have just one order ...
in this case order called owning side and module called invers side ...
and module keep id of order ...
look at this example
Table: Orders
id | user_id | ... | created_at |
----------------------------------------------------
1 | 12 | ... | 2018-07-28 00:00:00 |
----------------------------------------------------
Table: Modules
id | order_id | ... | created_at |
----------------------------------------------------
1 | 1 | ... | 2018-07-28 00:00:00 |
----------------------------------------------------
2 | 1 | ... | 2018-07-29 00:00:00 |
----------------------------------------------------
you must write your code like this...
Order Class
class Order implements OrderInterface
{
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="Module", mappedBy="order", cascade={"persist"})
*/
protected $modules;
/**
* Don't forget initial your collection property
* Order constructor.
*/
public function __construct()
{
$this->modules = new ArrayCollection();
}
/**
* #return Collection
*/
public function getModules(): Collection
{
return $this->modules;
}
/**
* #param ModuleInterface $module
*/
public function addModule(ModuleInterface $module): void
{
if ($this->getModules()->contains($module)) {
return;
} else {
$this->getModules()->add($module);
$module->setOrder($this);
}
}
/**
* #param ModuleInterface $module
*/
public function removeModule(ModuleInterface $module): void
{
if (!$this->getModules()->contains($module)) {
return;
} else {
$this->getModules()->removeElement($module);
$module->removeOrder($this);
}
}
}
Module Class
class Module implements ModuleInterface
{
/**
* #var OrderInterface
*
* #ORM\OneToMany(targetEntity="Order", mappedBy="modules", cascade={"persist"})
*/
protected $order;
/**
* #param OrderInterface $order
*/
public function setOrder(OrderInterface $order)
{
$this->order = order;
}
public function getOrder(): OrderInterface
{
return $this->order;
}
}
when you persist order object by doctrine... doctrine handle this and create items
My User model is like this :
class User extends Authenticatable
{
protected $primaryKey = 'user_id';
protected $fillable = [
'username',
'password',
'name',
'family',
'supervisor'
];
public function children()
{
return $this->hasMany(\App\User::class, 'supervisor', 'user_id')->with('children');
}
}
As you can see there a supervisor column that specify parent of a user.
Now to fetch all children of user models that have supervisor= null, I wrote this :
return User::with('children')->whereNull('supervisor')->get();
but it return this error always :
PHP Warning: Illegal offset type in D:\wamp\www\zarsam-app\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\HasOneOrMany.php on line 168
Table users have these data :
+---------+-------------+---------+--------------+------------+
| user_id | username | name | family | supervisor |
+---------+-------------+---------+--------------+------------+
| 1 | 09139616246 | ahmad | badpey | null |
| 7 | alinasiri | ali | nasiri arani | 1 |
| 8 | zahedi | mostafa | zahedi | 1 |
| 9 | hsan | hasan | ghanati | 8 |
+---------+-------------+---------+--------------+------------+
Update :
I found that problem is that I have a accessor same name supervisor attribute like this :
public function getSupervisorAttribute($value)
{
return is_null($value) ? null : User::select('user_id', 'name', 'family')->find($value);
}
I added that because I want to return supervisor user as an object.
But now in this case, what do I do ?
Try with this setup:
public function children()
{
return $this->hasMany(\App\User::class, 'supervisor', 'user_id');
}
And fetching them like
return User::with('children.children')->whereNull('supervisor')->get();
If this doesn't resolve your issue, then your relationships are not well defined, then I suggest reading about foreignand local keys inside your children.children model
NOTE:
HasOneOrMany.php
...
return $results->mapToDictionary(function ($result) use ($foreign) { //line in question
...
And when I found that method, this is what it said:
/**
* Run a dictionary map over the items.
*
* The callback should return an associative array with a single key/value pair.
*
* #param callable $callback
* #return static
*/
public function mapToDictionary(callable $callback)
I want to point out The callback should return an associative array with a single key/value pair. Which means you cannot do it recursively, at least I believe you can't.
I am just discovering Laravel, and getting into Eloquent ORM. But I am stumbling on a little issue that is the following.
I have three tables with the following structures and data :
words
id | language_id | parent_id | word
-------------------------------------------
1 | 1 | 0 | Welcome
-------------------------------------------
2 | 2 | 1 | Bienvenue
-------------------------------------------
documents
id | title
---------------------
1 | Hello World
---------------------
documents_words
document_id | word_id
--------------------------
1 | 1
--------------------------
As you see, we have a parent/child relationship in the words table.
The documents Model is defined as following
class Documents extends Eloquent {
protected $table = 'documents';
public function words()
{
return $this->belongsToMany('Word', 'documents_words', 'document_id');
}
}
And the words model :
class Word extends Eloquent {
protected $table = 'words';
public function translation()
{
return $this->hasOne('Word', 'parent_id');
}
}
Now my problem is that I want to retrieve documents that have translated words, so I thought this would do it :
$documents = Documents::whereHas('words', function($q)
{
$q->has('translation');
})
->get();
But I get 0 results, so I checked the query that Eloquent generates and uses :
select * from `prefix_documents`
where
(
select count(*) from
`prefix_words`
inner join `prefix_documents_words`
on `prefix_words`.`id` = `prefix_documents_words`.`word_id`
where `prefix_documents_words`.`document_id` = `prefix_documents`.`id`
and (select count(*)
from `prefix_words`
where `prefix_words`.`parent_id` = `prefix_words`.`id`) >= 1
) >= 1
The problem is that it doesn't use aliases for the tables, what my query should be more like this to work (and it does) :
select * from `prefix_documents`
where
(
select count(*) from
`prefix_words`
inner join `prefix_documents_words`
on `prefix_words`.`id` = `prefix_documents_words`.`word_id`
where `prefix_documents_words`.`document_id` = `prefix_documents`.`id`
and (select count(*)
from `prefix_words` as `w`
where `w`.`parent_id` = `prefix_words`.`id`) >= 1
) >= 1
But how can I do this with Eloquent ORM ?
Thanks a lot for your help guys, hope I am clear enough.
In the Word Model, change the
public function translation()
{
return $this->hasOne('Word', 'parent_id');
}
to
public function translation()
{
return $this->belongsToMany('Word', 'words', 'id', 'parent_id');
}
This way we are telling the Laravel to create an alias in the eloquent when using your query. I didn't test the other cases, but I think it will work.