How to avoid ambiguous field while reusing query shorthand in Yii2? - php

I have table like below.
CREATE TABLE A (
id INT,
relationId INT,
status INT
)
CREATE TABLE B (
id INT,
status INT
)
The class file is like below
class A extends \yii\db\ActiveRecord {
public function getB() {
return $this->hasOne(B::class, ['id' => 'relationId']);
}
public function find() {
return new AQuery(__CLASS__);
}
}
class AQuery extends \yii\db\Query {
public function isActive() {
return $this->andWhere(['status' => 1]);
}
public function isNotActive() {
return $this->andWhere(['status' => 0]);
}
}
class B extends \yii\db\ActiveRecord {
public function find() {
return new BQuery(__CLASS__);
}
}
class BQuery extends \yii\db\Query {
public function isActive() {
return $this->andWhere(['status' => 1]);
}
public function isNotActive() {
return $this->andWhere(['status' => 0]);
}
}
I'm doing something like this
$model = A::find()
->joinWith([
'b' => function(BQuery $query) {
$query->isNotActive();
}
])
->isActive()
->one();
This will produce error
Column 'status' in where clause is ambiguous"
The only way I know is to manually add alias to $query->from and rewrite the $query->andWhere. But is there any easier way to reuse the query shorthand?

Use ActiveRecord::tableName() instead of aliasing (which doesn't seem to be an active record feature in Yii2). The tableName() can be accessed through the modelClass property of \yii\db\ActiveQuery.
public function isActive() {
$modelClass = $this->modelClass;
return $this->andWhere([$modelClass::tableName().'.status' => 1]);
}

You can enhance your isActive() method to accept aliases with an optionnal parameter. You can try something like this:
class AQuery extends \yii\db\Query {
protected function getAlias($alias = null) {
return $alias !== null ? $alias : A::tableName();
}
public function isActive($alias = null) {
$alias = $this->getAlias($alias);
return $this->andWhere(["{$alias}.status" => 1]);
}
public function isNotActive($alias = null) {
$alias = $this->getAlias($alias);
return $this->andWhere(["{$alias}.status" => 0]);
}
}

Related

Laravel find() does not get data from the DB

I have a resource Controller with this index method like this:
public function index()
{
$args = [];
$args = array_merge($args, $this->data_creator(35, 12, 'book'));
$args = array_merge($args, $this->data_creator(37, 12, 'kit'));
$args = array_merge($args, $this->data_creator(38, 12, 'game'));
$args['menu_links'] = [
'books' => route('shopping-products.category', Category::find(25)->slug),
'videos' => route('shopping-products.category', Category::find(24)->slug),
'kits' => route('shopping-products.category', Category::find(23)->slug),
'games' => route('shopping-products.category', Category::find(22)->slug),
];
return view('frontend.shop.products.index', $args);
}
But it returns this error:
Trying to get property 'slug' of non-object
And when I dd(Category::find(25), Category::find(24), Category::find(23), Category::find(22)); I get NULL results.
Meaning that it can not find data with specified ids.
However there are 25 records stored at the categories table:
So what is going wrong here? How can I fix this issue?
I would really appreciate any idea or suggestion from you guys...
Thanks in advance.
Here is Category.php Model:
class Category extends Model
{
use Sluggable, SoftDeletes;
protected $table = 'categories';
protected $primaryKey = 'cat_id';
protected $guarded = [];
/**
* Return the sluggable configuration array for this model.
*
* #return array
*/
public function sluggable()
{
return [
'slug' => [
'source' => 'cat_name'
]
];
}
public function path()
{
return "/products/categories/$this->slug";
}
public function children()
{
return $this->hasMany(Category::class, 'cat_parent_id', 'cat_id');
}
public function parents()
{
return $this->hasMany(Category::class, 'cat_id', 'cat_parent_id');
}
public function products()
{
return $this->belongsToMany(Product::class, 'category_products', 'ctp_cat_id', 'ctp_prd_id');
}
public function news()
{
return $this->belongsToMany(News::class, 'category_news', 'ctn_cat_id', 'ctn_nws_id');
}
public function galleries()
{
return $this->belongsToMany(Gallery::class, 'category_galleries', 'ctg_cat_id', 'ctg_gly_id');
}
public function uploaded()
{
return $this->hasMany(UploadedFile::class, 'upf_object_id', 'cat_id')->where('upf_object_type_id', '=', '107');
}
public function articles()
{
return $this->belongsToMany(Article::class, 'article_category', 'act_cat_id', 'act_art_id');
}
public function olympiadExam()
{
return $this->belongsToMany(OlympiadExam::class, 'olympiads_exams_categories', 'oec_ole_id', 'oec_cat_id');
}
public function olympiadExamQuestion()
{
return $this->belongsToMany(OlympiadExamQuestion::class, 'olympiads_exams_questions_categories', 'oes_cat_id', 'oes_oeq_id')->orderBy('oeq_number', 'asc');
}
public function attr_attributes()
{
return $this->hasMany(CategoryAttribute::class, 'category_id', 'cat_id');
} //
public function attr_product()
{
return $this->hasMany(Product::class, 'prd_cat_att_id', 'cat_id');
} //
public function couponRelation()
{
return $this->hasMany(couponRelation::class, 'object_id', 'cat_id')->where('object_type', 'product_category');
}
public function magazines()
{
return $this->belongsToMany(Magazine::class, 'category_magazine', 'category_id', 'magazine_id');
}
}
And when I do: dd(Category::where('cat_id', 25), Category::where('cat_id', 24), Category::where('cat_id', 23), Category::where('cat_id', 22)); I get this as result:
The problem is because you are using SoftDeletes so soft deleted models will automatically be excluded from query results. In your case, look like Category with id 22, 23, 24, 25 are soft deleted. To get it, you need to use withTrashed() as mentioned in the doc. For example:
Category::withTrashed()->find(22)->slug
per an answer above: if you are using soft deletes you need to add
Category::withTrashed()
However, you can wrap the command in an optional() helper function.
optional(Category::find(22))->slug
// if you are using soft delete
optional( Category::withTrashed()->find(22) )->slug
this will return null if 22 does not exist instead of throwing an exception error.

Use eloquent relation on models returning by another method

I have a Category model which has belongsToMany relation with Product model via a pivot table called product_to_category
I can get all products in a Category with $category->products() and then apply a Filter scope to it to filter the result with parameters given in Request like this:
When I send this request :
http://site.dev/category/205?product&available&brand
I apply the parameters like this:
Category::find($id)->products()->filter($request)
The problem is when I want to get all product in a category and its children. The existing products relation gives me products in only given category.
I tried to modify the products() method in Category model as this:
public function products()
{
return DB::table('oc_product')
->join('oc_product_to_category', 'oc_product_to_category.category_id', '=', 'oc_product_to_category.category_id')
->join('oc_category_path', 'oc_category_path.category_id', '=', 'oc_category.category_id')
->whereIn('oc_product_to_category.category_id', $this->children(true));
}
But when I this code :
Category::find($id)->products()->filter($request)
I get this exception error:
(1/1) BadMethodCallException
Call to undefined method Illuminate\Database\Query\Builder::filter()
I know that filter scope is defined in Model class, but how can I apply that filter scope to QueryBuilder which is returned by modified products method?
Here are my classes :
Product model:
class Product extends Model {
public function scopeFilter( $request, QueryFilter $filters ) {
return $filters->apply( $request );
}
public function categories() {
return $this->belongsToMany( Category::class, 'product_to_category', 'product_id', 'category_id' );
}
}
Category model:
class Category extends Model
{
public function scopeFilter($query, QueryFilter $filters)
{
return $filters->apply($query);
}
public function children($id_only = false)
{
$ids = $this->hasMany(CategoryPath::class, 'path_id', 'category_id')
->join('category', 'category.category_id', '=', 'category_path.category_id')
->where('category.status', 1)
->pluck('category.category_id');
if ($id_only)
return $ids;
return self::find($ids);
}
public function parent()
{
$parent = DB::Select("SELECT cp.path_id AS category_id FROM category_path cp LEFT JOIN category_description cd1
ON (cp.path_id = cd1.category_id AND cp.category_id != cp.path_id)
WHERE cd1.language_id = '2' AND cp.category_id = " . $this->category_id);
return $parent;
}
public function products()
{
return $this->belongsToMany(Product::class, 'product_to_category', 'category_id', 'product_id');
}
}
QueryFilter class:
abstract class QueryFilter {
protected $request;
protected $builder;
public function __construct( Request $request ) {
$this->request = $request;
}
public function filters() {
return $this->request->all();
}
public function apply( Builder $builder ) {
$this->builder = $builder;
foreach ( $this->filters() as $name => $value) {
if (method_exists($this, $name)) {
call_user_func_array([$this, $name], array_filter([$value]));
}
}
return $this->builder;
}
}
CategoryFilter class:
class CategoryFilters extends QueryFilter
{
public function id($id)
{
return $this->builder->where('category_id', $id);
}
public function procons()
{
return $this->builder->with('pros', 'cons');
}
public function available()
{
return $this->builder->where('quantity', '>', 0);
}
public function optionValues()
{
return $this->builder->with('optionValues');
}
public function description()
{
return $this->builder->with('description');
}
public function images()
{
return $this->builder->with('images');
}
public function order($order)
{
$params = explode(',', $order);
$order = isset($params[0]) ? $params[0] : null;
$way = isset($params[1]) && strtolower($params[1]) == 'desc' ? $params[1] : 'asc';
if ($order) {
return $this->builder->orderBy($order, $way);
}
return $this->builder;
}
}

Laravel query with multiple where not returning expected result

I'm trying to build a query from a Repository in a Model with 2 where clauses.
This is the data I have in a MySql table:
id name environment_hash
1 online_debit abc
2 credit_cart abc
I want to query by name and environment_hash. To do this, I created the method findByHashAndMethod() (see below).
But when I use it in my controller, like this:
$online_debit = $this->ecommercePaymentMethodRepository->findByHashAndMethod($hash, 'online_debit')->first();
or this:
$credit_card = $this->ecommercePaymentMethodRepository->findByHashAndMethod($hash, 'credit_cart')->first();
I keep getting both rows and not only the ones filtered. What's wrong with the code?
This is my PaymentMethodRepository.php
class EcommercePaymentMethodRepository extends BaseRepository
{
public function findByHashAndMethod($hash = null, $payment_method)
{
$model = $this->model;
if($hash)
{
$filters = ['environment_hash' => $hash, 'name' => $payment_method];
$this->model->where($filters);
}
else
{
$this->model->where('environment_hash', Auth::user()->environment_hash)
->where('name', $payment_method);
}
return $model;
}
public function model()
{
return EcommercePaymentMethod::class;
}
}
And this is my model EcommercePaymentMethod.php
<?php
namespace App\Models;
use Eloquent as Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class EcommercePaymentMethod extends Model
{
use SoftDeletes;
public $table = "ecommerce_payment_methods";
protected $dates = ['deleted_at'];
public $fillable = [
"name",
"payment_processor_id",
"active",
"environment_hash"
];
protected $casts = [
"name" => "string"
];
public function payment_processor()
{
return $this->hasOne('App\Models\EcommercePaymentProcessor');
}
}
While I am not entirely sure why ->first() would ever return more than one result, your Repository method had some few glaring issues that's prone to errors.
class EcommercePaymentMethodRepository extends BaseRepository
{
// 1. Do not put optional parameter BEFORE non-optional
public function findByHashAndMethod($payment_method, $hash = null)
{
// 2. Call ->model() method
$model = new $this->model();
// 3. Logic cleanup
if (is_null($hash)) {
$hash = Auth::user()->environment_hash;
}
return $model->where('environment_hash', $hash)
->where('name', $payment_method);
}
public function model()
{
return EcommercePaymentMethod::class;
}
}

Sort by selectRaw relationship in Laravel (calculated count, sum ...)

I have a Question model which have morphMany Answer. Answer have also morphMany Answer and Rating
I added relationship to calculate and load Answer count and Answer rating sum :
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use App\Facades\AuthManager;
class Answer extends Model {
protected $with = [
'user',
'cards',
'answers.user',
'answersCountRelation',
'ratingSumRelation'
];
protected $fillable = [
'text',
'user_id'
];
public function answerable() {
return $this->morphTo();
}
public function user() {
return $this->belongsTo(User::class);
}
public function answers() {
return $this->morphMany(Answer::class, 'answerable');
}
public function reports() {
return $this->morphMany(Report::class, 'reportable');
}
public function cards() {
return $this->hasMany(Card::class);
}
public function ratings() {
return $this->hasMany(Rating::class);
}
public function hasReported(User $user) {
return ($user != null && $this->reports()->where([
'user_id' => $user->id
])->first() != null);
}
public function hasRated(User $user) {
return ($user != null && $this->ratings()->where([
'user_id' => $user->id
])->first() != null);
}
public function answersCountRelation() {
return $this->answers()->selectRaw('answerable_id, count(*) as count')->groupBy('answerable_id');
}
public function getAnswersCountAttribute() {
return ($this->answersCountRelation->first() ? $this->answersCountRelation->first()->count : 0);
}
public function ratingSumRelation() {
return $this->ratings()->selectRaw('answer_id, SUM(power) as sum')->groupBy('answer_id');
}
public function getRatingSumAttribute() {
return ($this->ratingSumRelation->first() ? $this->ratingSumRelation->first()->sum : 0);
}
}
Now I wonder how can I load Question with Answer already sorted by answer count, rating, created_at etc
Is it possible to do it using the already eager loaded answersCountRelation or ratingSumRelation ?
Or do I have to do another request in Question->with(['answers' => function ($q) ...
Thx !

Get distinct attribute from database in Laravel

I have two tables, one called "products" and another "product_brands".
A product has one brand, and a brand can belong to many products.
I have:
class Product extends Eloquent {
protected $table = 'products';
public function type() {
return $this->hasOne('ProductTypes');
}
public function brand()
{
return $this->hasOne('ProductBrands', 'id', 'brand_id');
}
public function image() {
return $this->hasMany('ProductImages');
}
public function toArray() {
$ar = $this->attributes;
$ar['type'] = $this->type;
$ar['brand'] = $this->brand;
return $ar;
}
public function getBrandAttribute() {
$brand = $this->brand()->first();
return (isset($brand->brand) ? $brand->brand : '');
}
}
And my controller:
class ProductsController extends BaseController {
public function index($type_id) {
$Product = new Product;
$products = $Product->where('type_id', $type_id)->get();
return View::make('products.products', array('products' => $products));
}
}
Ideally I would like the column from "product_brands" to be in the same array as the columns from "products", hence why I am trying that stuff with toArray() and getBrandAttribute() but it isn't working.
How can I do this?
I'm sure the getBrandAttribute accessor collides with the brand relationship. Try this instead:
class Product extends Eloquent {
protected $table = 'products';
public function type() {
return $this->hasOne('ProductTypes');
}
public function productBrand() {
return $this->hasOne('ProductBrands', 'id', 'brand_id');
}
public function image() {
return $this->hasMany('ProductImages');
}
public function getBrandAttribute() {
$brand = $this->productBrand()->first();
return (isset($brand->brand) ? $brand->brand : '');
}
protected $appends = array('brand'); // this makes Laravel include the property in toArray
}
You should change your accessor to other name:
public function getSpecBrandAttribute() {
$brand = $this->brand()->first();
return (isset($brand->brand) ? $brand->brand : '');
}
and in toArray you should then use:
public function toArray() {
$ar = $this->attributes;
$ar['type'] = $this->type;
$ar['brand'] = $this->spec_brand;
return $ar;
}
That's because you shouldn't create fields with the same name as relationship name.
In addition as it's one to many relationship, probably for brand() you should use belongsTo and not hasOne

Categories