How to compare Query conditions from a BelongsToMany association in CakePHP 4.x? - query-builder

I have a working Posts, Categories and Tags relationship. (Posts BelongsTo Categories and BelongsToMany Tags.) They work just fine at my view and index actions with no issue.
Now, for a simple "search" functionality I'm working with the Query builder. I managed to make it successfully search Posts related to my query, as long as the terms are compared to fields from Posts and Categories, but I would also want to make it work with Tags.
This is my (working) Controller:
public function search()
{
$search = $this->request->getQuery('query');
$posts = $this->Posts->find('all');
$posts->contain(['Categories','Tags']);
if(!empty($search)) {
$posts->where(['or' => [
['Posts.title LIKE' => '%'.$search.'%', 'Posts.status' => 'published'],
['Posts.content LIKE' => '%'.$search.'%', 'Posts.status' => 'published'],
['Categories.name LIKE' => '%'.$search.'%','Posts.status' => 'published'],
]]);
} else {
$posts->where(['Posts.status' => 'published']);
};
$posts = $this->paginate($posts);
$this->set(compact('posts'));
}
These are my (working) Models:
// Posts Table
class PostsTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('posts');
$this->setDisplayField('title');
$this->setPrimaryKey('id');
$this->belongsTo('Categories', [
'foreignKey' => 'category_id',
'joinType' => 'INNER',
]);
$this->belongsToMany('Tags',[
'foreignKey' => 'post_id',
'targetForeignKey' => 'tag_id',
'joinTable' => 'posts_tags'
]);
}
}
// Categories Table
class CategoriesTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('categories');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->hasMany('Posts', [
'foreignKey' => 'category_id',
]);
}
}
// Tags Table
class TagsTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('tags');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->belongsToMany('Posts', [
'foreignKey' => 'tag_id',
'targetForeignKey' => 'post_id',
'joinTable' => 'posts_tags',
]);
}
}
// PostsTags Table
class PostsTagsTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('posts_tags');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->belongsTo('Posts', [
'foreignKey' => 'post_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Tags', [
'foreignKey' => 'tag_id',
'joinType' => 'INNER',
]);
}
}
And this is my view:
<?php $search = $this->request->getQuery('query'); ?>
<div class="posts index content">
<h1>Search Posts</h1>
<?= $this->Form->create(NULL,['type' => 'get']) ?>
<?= $this->Form->control('query',['default' => $search]) ?>
<?= $this->Form->button('submit') ?>
<?= $this->Form->end() ?>
<?php foreach ($posts as $post): ?>
<div class="card">
<!-- Here goes the Post data -->
</div>
<?php endforeach; ?>
</div>
<div class="paginator">
<ul class="pagination">
<?= $this->Paginator->first('<< ' . __('first')) ?>
<?= $this->Paginator->prev('< ' . __('previous')) ?>
<?= $this->Paginator->numbers() ?>
<?= $this->Paginator->next(__('next') . ' >') ?>
<?= $this->Paginator->last(__('last') . ' >>') ?>
</ul>
<p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}')) ?></p>
</div>
So when I submit the form, it filters my posts according to those conditions. But when I try adding a field from my Tags model to the search query, it breaks.
I tried adding the line:
['Tags.name LIKE' => '%'.$search.'%', 'Posts.status' => 'published']
...under:
['Categories.name LIKE' => '%'.$search.'%','Posts.status' => 'published']
But then when I introduce a query term, it throws me a "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Tags.name' in 'where clause'" error.
Same thing happens if instead of "$posts->where(...)" I use the "$posts->find('all',['conditions' => [...]]):" option.
So I'm stumped... How can I search a term within a HABTM relationship?
What am I missing?

User ndm's comment did the trick. So in case it wasn't clear enough (and if someone ever finds the same issue I did in the future), here's my final working controller:
public function search()
{
$search = $this->request->getQuery('query');
$posts = $this->Posts->find('all')
->leftJoinWith('Tags')
->group(['Posts.id']);
$posts->contain(['Categories','Tags']);
if(!empty($search)) {
$posts->where(['or' => [
['Posts.title LIKE' => '%'.$search.'%', 'Posts.status' => 'published'],
['Posts.content LIKE' => '%'.$search.'%', 'Posts.status' => 'published'],
['Categories.name LIKE' => '%'.$search.'%','Posts.status' => 'published'],
['Tags.name LIKE' => '%'.$search.'%','Posts.status' => 'published']
]]);
} else {
$posts->where(['Posts.status' => 'published']);
};
$posts = $this->paginate($posts);
$this->set(compact('posts'));
}

Related

Add date into table which is in relation with product in Laravel 6

I have product and auction table. I want to add auction deadline on a specific table using form. when I submit the form the product_id in auction table is not populated and deadline shows time on which form is submited.
Here what I am trying:
I want that the create form get the deadline and store in auction table with product id so I can access it in show method of product.
create.blade.php
#extends('layouts.app')
#section('content')
<div class="mt-3" style="margin-left: 50px;">
<h2>Add new product</h2>
{!! Form::open(['action' => 'ProductsController#store', 'method' => 'POST', 'enctype' =>
'multipart/form-data', 'class' => 'w-50 py-3']) !!}
<div class="form-group">
{{Form::label('name', 'Product Name')}}
{{Form::text('name', '', ['class' => 'form-control', 'placeholder' => 'Product Name'])}}
</div>
<div class="form-group">
{{Form::label('description', 'Product Description')}}
{{Form::textarea('description', '', ['class' => 'form-control', 'placeholder' => 'Product Description', 'rows' => '4'])}}
</div>
<div class="form-group">
{!! Form::Label('category', 'Category') !!}
<select class="form-control" name="category_id">
#foreach($categories as $category)
<option value="{{$category->id}}">{{$category->name}}</option>
#endforeach
</select>
</div>
<div class="form-group">
{{Form::label('price', 'Product Price')}}
{{Form::number('price', '', ['class' => 'form-control', 'placeholder' => 'Product Price'])}}
</div>
<div class="form-group">
{{Form::label('deadline', 'Auction Deadline')}}
{{Form::date('{{$auction->deadline}}', '', ['class' => 'form-control'])}}
</div>
<div class="form-group">
{{Form::label('image', 'Product Image')}}
{{Form::file('image', ['class' => '', 'placeholder' => 'Product Image'])}}
</div>
{{Form::submit('Upload Product', ['class' => 'btn btn-primary'])}}
{!! Form::close() !!}
</div>
#endsection
Product Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'name', 'price', 'description', 'image',
];
public function category()
{
return $this->belongsTo('App\Category');
}
public function auction()
{
return $this->hasOne('App\Auction');
}
}
ProductsController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use App\Product;
use App\Category;
use App\Auction;
class ProductsController extends Controller
{
public function index()
{
$categories = Category::all();
$products = Product::with('category')->latest()->paginate(3);
return view('products.index' ,compact('categories', 'products'));
}
public function create()
{
$categories = Category::all(['id', 'name']);
return view('products.create', compact('categories',$categories));
}
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required',
'description' => 'required',
'category_id' => 'required',
'price' => 'required',
'image' => 'image|nullable',
]);
// Create Product
$product = new Product();
$product->name = request('name');
$product->description = request('description');
$product->category_id = request('category_id');
$product->price = request('price');
$product->image = $fileNameToStore;
$product->save();
$auction = new Auction();
$auction->deadline = request('deadline');
$auction->save();
return redirect('/products')->with('success', 'Product Created');
}
public function show($id)
{
$product = Product::find($id);
return view('products.show', compact('product'));
}
}
you are just creating auction, where is the relation? Delete $product->save(); line. After the $auction->save(); add this line:
$product->auction()->associate($auction);
$product->save();
Final Store method
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required',
'description' => 'required',
'category_id' => 'required',
'price' => 'required',
'image' => 'image|nullable',
]);
// Create Product
$product = new Product();
$product->name = request('name');
$product->description = request('description');
$product->category_id = request('category_id');
$product->price = request('price');
$product->image = $fileNameToStore;
$auction = new Auction();
$auction->deadline = request('deadline');
$auction->save();
$product->auction()->associate($auction);
$product->save();
return redirect('/products')->with('success', 'Product Created');
}
Update for Irrelevant Blade problem
This line is wrong:
{{Form::date('{{$auction->deadline}}', '', ['class' => 'form-control'])}}
You are using blade close string tag in blade string tag.
Should be:
{{Form::date('deadline', '', ['class' => 'form-control'])}}
If auction variable has been passing from controller this will work

Yii2 Adding rules in models if the record is already exists in database

My problem is validating the records if already exists in database. So i created a Officials Form using Gii Generator in yii2. It contains name_id,position,fname,mname,lname. If the admin wants to create a new data, and that data is already exists it will show a pop-up message "This data already exists".
How can i achieve that? Any ideas?
This is my model:
class Name extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'name';
}
public function rules()
{
return [
[['position', 'fname', 'lname'], 'required'],
[['position', 'fname', 'mname', 'lname'], 'string', 'max' => 50],
];
}
public function attributeLabels()
{
return [
'name_id' => 'Name ID',
'position' => 'Position',
'fname' => 'First Name',
'mname' => 'Middle Name',
'lname' => 'Last Name',
];
}
}
This is my controller:
public function actionCreate()
{
$model = new Name();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['report/create', 'id' => $model->name_id]);
} else {
return $this->renderAjax('create', [
'model' => $model,
]);
}
}
And this is my _form:
<div class="name-form">
<?php yii\widgets\Pjax::begin(['id' => 'sign-up']) ?>
<?php $form = ActiveForm::begin(['id' => 'form-signup', 'options' => ['data-pjax' => true]]); ?>
<?= $form->field($model, 'position')->textInput(['maxlength' => true,'style'=>'width:500px','placeholder' => 'Enter a Position....']) ?>
<?= $form->field($model, 'fname')->textInput(['maxlength' => true,'style'=>'width:500px','placeholder' => 'Enter a First Name....']) ?>
<?= $form->field($model, 'mname')->textInput(['maxlength' => true,'style'=>'width:500px','placeholder' => 'Enter a Middle Name....']) ?>
<?= $form->field($model, 'lname')->textInput(['maxlength' => true,'style'=>'width:500px','placeholder' => 'Enter a Last Name....' ]) ?>
<div class="form-group">
<?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-success','style' => 'padding:10px 60px; width:100%;']) ?>
</div>
<?php ActiveForm::end(); ?>
<?php yii\widgets\Pjax::end() ?>
So i assume you want a unique validator, you can try this in your model :
public function rules()
{
return [
[['position', 'fname', 'lname'], 'required'],
[['position', 'fname', 'mname', 'lname'], 'string', 'max' => 50],
[['fname','lname'], 'unique', 'targetAttribute' => ['fname', 'lname'], 'message' => 'This data already exists']
];
}
The above rules will make the combination of fname and lname attributes to be unique, you can modify what attribute or combination of attributes you want to be unique by adding or removing the field name / attributes to the validation rules.
You can create your custom validation , Take any attribute name and define rule :
public function rules()
{
return [
[['position', 'fname', 'lname'], 'required'],
[['position', 'fname', 'mname', 'lname'], 'string', 'max' => 50],
[['position', 'fname', 'lname'], 'checkUniq'], //add this line
];
}
-> custom function in model :
public function checkUniq($attribute, $params)
{
$user = self::find()->where(['fname'=>$this->fname,'lname'=>$this->lname,'position'=>$this->position])->one();
if (isset($user) && $user!=null)
$this->addError($attribute, 'User already added.');
}

Ajax validation with custom id in yii2

I have a same field in foreach loop like below
foreach ( $subCategoryData as $k => $val) {
<?= $form->field($model, 'sub_category', ['template' => '{input}'])->textInput(['maxlength' => 255, 'class' => 'form-control required section_name', 'name' => "Category[sub_category][$k][name]"]) ?>
} ?>
I have ajax validation with custom method it is working fine.
But it is Working with only first input. Because it has same ID.
But when I changed it with 'inputOptions' => ['id' => 'myCustomId'] and make it unique with below and my ajax validation is not called.
foreach ( $subCategoryData as $k => $val) {
<?= $form->field($model, 'sub_category', ['template' => '{input}','inputOptions' => ['id' => "category-sub_category_".$k]])->textInput(['maxlength' => 255, 'class' => 'form-control required section_name', 'name' => "Category[sub_category][$k][name]"]) ?>
}
I have seen this solution here
https://github.com/yiisoft/yii2/issues/7627
and also seen this https://stackoverflow.com/a/28460442/2286537
But nothing work
can anyone help me ?
Your question is different from the posts you introduced.
You should use loadMultiple.
Example:
if (\Yii::$app->request->isAjax) {
if (\yii\base\Model::loadMultiple($model,\Yii::$app->request->post())) {
\Yii::$app->response->format = Response::FORMAT_JSON;
echo json_encode(ActiveForm::validateMultiple($model));
\Yii::$app->end();
}
}
if ( \yii\base\Model::loadMultiple($model, Yii::$app->request->post()) && \yii\base\Model::validateMultiple($model)) {
foreach ($model as $models) {
$models->save(false);
}
in view:
<?php $form = ActiveForm::begin([
'enableAjaxValidation' => true,
]);

Cakephp 3.0 Issue in validation when we use different validation set

I have a custom form for Change Password functionality. But the validations are not properly working on this page.
The template file (change_password.ctp) is:
<div class="users index large-9 medium-8 columns content">
<?= $this->Form->create('change_password') ?>
<fieldset>
<legend><?= __('Change password') ?></legend>
<?= $this->Form->input('old_password', ['type' => 'password', 'label' => 'Old password']) ?>
<?= $this->Form->input('new_password', ['type' => 'password', 'label' => 'Password']) ?>
<?= $this->Form->input('confirm_password', ['type' => 'password', 'label' => 'Repeat password']) ?>
</fieldset>
<?= $this->Form->button(__('Change')) ?>
<?= $this->Form->end() ?>
</div>
The validation code included in a custom function in UsersTable.php page:
public function validationChangePassword(Validator $validator) {
$validator
->requirePresence('old_password', 'create')
->notEmpty('old_password');
$validator
->requirePresence('new_password', 'create')
->notEmpty('new_password');
$validator
->notEmpty('confirm_password')
->add('confirm_password', 'no-misspelling', [
'rule' => ['compareWith', 'new_password'],
'message' => 'Passwords are not equal',
]);
return $validator;
}
I included the changePassword() action in the controller UsersController.php
public function changePassword() {
$user = $this->Users->newEntity();
if (isset($this->request->data) && !empty($this->request->data)) {
$user = $this->Users->patchEntity($user, $this->request->data, [
'validate' => 'changePassword'
]);
if ($user->errors()) {
$this->Flash->success(__('Error'));
}
}
}
Update your action as below:
public function changePassword() {
$user = $this->Users->newEntity();
if (isset($this->request->data) && !empty($this->request->data)) {
$user = $this->Users->patchEntity($user, $this->request->data, [
'validate' => 'changePassword'
]);
if ($user->errors()) {
$this->Flash->success(__('Error'));
}
}
$this->set(compact('user'));
}

Yii Refresh Grid On DropDown change

In YII views folder i have test module and admin.php file to manage contents are below and i render form here where i put the code of form and dropdown in it , i want that grid refresh value of status change in dropdown
Suppose i select "Approved" than Grid show the data where status is approved
<?php
Yii::app()->clientScript->registerScript('dropdown', "
$('.dropdown-form form').submit(function(){
$('#testimonial-grid').yiiGridView('update', {
data: $(this).serialize()
});
return false;
});
");
?>
<h1>Manage Testimonials</h1>
<div class="dropdown-form">
<?php $this->renderPartial('_dropdownform',array(
'model'=>$model,
)); ?>
</div><!-- search-form -->
<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'testimonial-grid',
'dataProvider'=>$model->search(),
'filter'=>$model,
'columns'=>array(
'id',
'created_by',
'test_name',
'test_email',
'comments',
'created_at',
/*
'status',
'approved_on',
'approved_by',
*/
array(
'class'=>'CButtonColumn',
),
),
)); ?>
Form below is _dropdownform , it contain a form and dropdown from this dropdown i am choosing the value of status
<div class="wide form">
<?php
$form = $this->beginWidget('CActiveForm', array(
'action' => Yii::app()->createUrl($this->route),
'method' => 'get',
));
?>
<div class="row">
<?php
echo CHtml::dropDownList('status', '', array(0 => 'New', 1 => 'Approved', 2 => 'Declined'), array(
'prompt' => 'Select Status',
'ajax' => array(
'type' => 'POST',
'url' => Yii::app()->createUrl('testimonial/loadthedata'),
//or $this->createUrl('loadcities') if '$this' extends CController
'update' => '#testimonial-grid', //or 'success' => 'function(data){...handle the data in the way you want...}',
'data' => array('status' => 'js:this.value'),
)));
?>
</div>
<div class="row buttons">
<?php //echo CHtml::submitButton('Search'); ?>
</div>
<?php $this->endWidget(); ?>
</div><!-- search-form -->
AND THE CODE IN MY CONTROLLER OR URL GIVEN IN DROPDOWN TO FETCH DATA IS FOLLOWING ACTION BUT I DONT KNOW HOW TO FETCH DATA FROM THIS FUNCTION AND PASS TO GRID VIEW
public function actionloadthedata() {
if (isset($_POST['status'])) {
$status = $_POST['status'];
if($status==0){
$status='New';
}
if($status==1){
$status='Approved';
}
if($status==2){
$status='Declined';
}
Testimonial::model()->findByAttributes(array('status'=>$status));
}
}
You can use CGridView property filterCssClass to link the grid filter, for example
$this->widget('CGridView', array(
'id' => 'my-list',
'filterCssClass' => '#filterFormId .filter',
And there is filter form
<?php $form = $this->beginWidget('CActiveForm', array(
'id' => 'filter-fomr-id',
)); ?>
<div class="filter clearfix">
<?php echo $form->dropDownList($model, 'name', [0=>'all', '1'=>'some else']); ?>
</div>
Replace #filterFormId .filter on jquery selector specific to you form. In other words, set id attribute for filter form, then use "#THISID .row".
In your gridview file, make sure you have this code:
Yii::app()->clientScript->registerScript('search', "
$('.search-button').click(function(){
$('.search-form').toggle();
return false;
});
$('.search-form form').submit(function(){
$('#ad-grid').yiiGridView('update', {
data: $(this).serialize()
});
return false;
});
");
then in the CGridView definition:
$this->widget('zii.widgets.grid.CGridView', array(
'id'=>'testimonial-grid',
'dataProvider'=>$model->search(),
'filter'=>$model,
'columns'=>array(
...
array(
'name'=>'Status',
'filter'=>CHtml::dropDownList('YourModel[status]', $model->status, array(0 => 'New', 1 => 'Approved', 2 => 'Declined'), array('empty' => '--all--') ),
'value'=>'( $data->status == 0) ? "New": ( $data->status == 1) ? "Approved" : "Declined"',
'htmlOptions' => array(
'style' => 'width: 40px; text-align: center;',
),
),
...
array(
'class'=>'CButtonColumn',
),
),
));
// CGridView
In order to save if/else in the 'value' section, you can implement a method in your model that returns the string associated to the integer.
It works great, just with the default Yii admin.php view, which you can edit as much as you need.
Update
Added support for empty status, does not filter query results
Thanks #Alex for your help but i am successful to make filterdropdown for grid , code is following but will you please tell me that i want that grid show only values where status=New , how i can do when page load grid show values where status is New
but first i paste the working code of dropdown filter for grid
Here is my admin.php file
<?php
$this->breadcrumbs = array(
'Testimonials' => array('index'),
'Manage',
);
$this->menu = array(
array('label' => 'List Testimonial', 'url' => array('index')),
array('label' => 'Create Testimonial', 'url' => array('create')),
);
?>
<h1>Manage Testimonials</h1>
<!-----------drop down form------------->
<?php
Yii::app()->clientScript->registerScript('dropdownfilter', "
$('.dropdown-form form #staticid').change(function(){
$.fn.yiiGridView.update('testimonial-grid', {
data: $(this).serialize()
});
return false;
});
");
?>
<div class="dropdown-form">
<?php
$this->renderPartial('_dropdownfilter', array(
'model' => $model,
));
?>
</div><!-- search-form -->
<?php
$this->widget('zii.widgets.grid.CGridView', array(
'id' => 'testimonial-grid',
'dataProvider' => $model->search(),
// 'filter' => $model,
'columns' => array(
'id',
'created_by',
'test_name',
'test_email',
'comments',
'created_at',
'status',
array(
'class' => 'CButtonColumn',
),
),
));
?>
Here is my render partial form where i place the static drop dow
<div class="wide form">
<?php
$form = $this->beginWidget('CActiveForm', array(
'action' => Yii::app()->createUrl($this->route),
'method' => 'get',
));
?>
<div class="row">
<?php
echo CHtml::dropDownList('staticid', '', array('0' => 'New', '1' => 'Approved', '2' => 'Declined'), array(
// 'onChange' => 'this.form.submit()',
'ajax' => array(
'type' => 'POST', //request type
)));
?>
</div>
<?php $this->endWidget(); ?>
And Here is code of my adminaction in controller
public function actionAdmin() {
$model = new Testimonial('search');
$model->unsetAttributes(); // clear any default values
if (isset($_GET['staticid'])) {
$getStatus = $_GET['staticid'];
if ($getStatus == 0)
$status = 'New';
if ($getStatus == 1)
$status = 'Approved';
if ($getStatus == 2)
$status = 'Declined';
$model->status = $status;
}
if (isset($_GET['Testimonial']))
$model->attributes = $_GET['Testimonial'];
$this->render('admin', array(
'model' => $model,
));
}
Now i want that when i actionadmin triggered first time it show status=New values

Resources