I am new to yii. I have tried myself,googled and found that yii2 default CRUD generator Gii will not generate CRUD for tables which has many to many relations. Also found that yii achieves(not in the sense Gii) Many to Many via One to Many yiiDrills .
Now I am trying to emulate the same kind of default CRUD manually with the help of Github issue trail and stackoverflow trail. I am facing the below issues while trying this.
Issue-1 (Model class of the table with Many to Many relations): Not able to initialize the class ActiveDataProvider,
$query = TableA::find();
$dataProvider = new ActiveDataProvider([
'query' => $query->TableB(),
]);
Issue-2(View): Even if I were able to initialize it how to render it via GridView
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
//How to give the column names like we give for one to many
/* Example */
'TableA.attr1',
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
Also I would like to know if it is desired to create a Model class for the table with Many to Many relations to handle CRUD.
Thanks
You should create models for all tables.
Example relations
/**
* #return \yii\db\ActiveQuery
*/
public function getCountry()
{
return $this->hasOne(Country::className(), ['id' => 'country_id']);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getCity()
{
return $this->hasOne(City::className(), ['id' => 'city_id']);
}
Example "Search" method
$query = Tour::find();
// Important: lets join the query with our previously mentioned relations
// I do not make any other configuration like aliases or whatever, feel free
// to investigate that your self
$query->joinWith(['city', 'country']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
You can replace hasOne with hasMany in your case.
Please check the link http://www.yiiframework.com/wiki/653/displaying-sorting-and-filtering-model-relations-on-a-gridview/
Related
I'm trying the 4.1 new feature "Inline create", but I can't seem to associate the ids of the items created. Let me explain what I'm doing / what I want:
I have "Folders" that have "Chapters" inside (so 1-n relation).
My code:
CRUD::addField([ //Folder crud
'name' => 'chapters',
'type' => 'relationship',
'label' => 'Unidad',
'model' => "App\Models\Chapter",
'inline_create' => [
'entity' => 'chapter',
'modal_class' => 'modal-dialog modal-xl',
'modal_route' => route('chapter-inline-create'),
'create_route' => route('chapter-inline-create-save'),
]
]);
protected function setupCreateOperation() //Chapter crud
{
CRUD::setValidation(ChapterRequest::class);
CRUD::addField([
'name' => 'name',
'type' => 'text',
'label' => 'Nombre'
]);
}
public function chapters() //Folder model
{
return $this->hasMany(Chapter::class);
}
public function folder() //Chapter model
{
return $this->belongsTo(Folder::class);
}
It creates the main item and the related items no problem, but it doesn't actually relate them in the database at any point.
Any clue of what I might be doing wrong? Followed the docs but can't seem to make it work.
Thank you.
Do you have the right column names in the db ? The columns that are making the relationship possible, i.e in the folder table you should have a column named something like chapter_name or chapter_id, to identify the chapter where the folder belongs to.
Moreover, if those columns do not follow laravel conventions you need to add them as the second and third parameter when you are implementing the relationship in the models
More details here https://laravel.com/docs/8.x/eloquent-relationships#one-to-many
One note on this... I was running into this issue and realized that I forgot to make the parent_id fillable on my child model.
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'parent_id',
]
I have a model Certificates which has a foreign key application_id of another model called Application. So each certificate belongs to the single application.
Now there is a situation where I would like to show all the certificates of the existing user. The user id exist inside the application model like user_id.
This is the query
SELECT * FROM `certificates`
inner join applications b ON
application_id = b.id where b.user_id = 7
Now based on the records coming from the above query I would like to show some columns of the certificates and some from the applications using grid view. But for some reasons, if records are more than one then I don't get any column data from the application.
<?php Pjax::begin(); ?> <?= GridView::widget([
'dataProvider' => $dataProvider,
// 'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'application_id',
'verified_application_file_path',
'certificate_name',
'application.name',
'application.user_id',
[
'attribute' => 'creation_date',
'format' => ['date', 'php:d/m/Y']
],
[
'attribute' => 'expiry_date',
'format' => ['date', 'php:d/m/Y']
],
],
]); ?>
<?php Pjax::end(); ?></div>
The above grid shows name and user id if a single record get return otherwise it shows "Not set". I Am not sure why 'application.name' and 'application.user_id'are not working when more than one records receive.
Here is my query using yii2
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search_vendor_certificates($user_id)
{
$query = ApplicationCertificates::find()->joinWith('application b',true , 'INNER JOIN')->where(["b.user_id"=>$user_id]);
// add conditions that should always apply here
$dataProvider = new \yii\data\ActiveDataProvider([
'query' => $query,
]);
return $dataProvider; }
I will appreciate if someone would tell me what is the mistake I am doing in displaying the proper attributes of the application model.
First of all (don't use this, i'll show u an logical mistake):
->joinWith('application b',true , 'INNER JOIN')
U set alias for application b, and then in GridView use application. Anyway, it's still bad if u rename it to b in GridView.
Based on this answer:
Model TableTwo:
public function getTableOneRecord()
{
return $this->hasOne(TableOne::className(), ['id' => 't1_id']);
}
public function getTableThreeRecord()
{
return $this->hasOne(TableThree::className(), ['id' => 't3_id']);
}
public function getTableFourRecord()
{
return $this->hasOne(TableFour::className(), ['id' => 't4_id'])
->via('tableThreeRecord');
}
And the view file:
echo GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
'id',
't1_id',
'tableOneRecord.id',
't3_id',
'tableThreeRecord.id',
'tableThreeRecord.t4_id',
'tableFourRecord.id',
],
]);
In easiest way to say, your relation from search won't work in GridView, you have to define relations in Model and then use thems ;)
See the code on GitHub
TL;DR
I have a query that works in RAW SQL but i have had little success recreating it with query builder or active record.
I am working on a web application based off of the yii2 advanced application template. I have written a database query and implemented it with findbysql() returning the correct records but am having trouble translating this into active record.
I originally wanted to allow the user to modify (filter) the results by means of a search form(user & date), however i have since realized that implementing filters on the gridview with active records would be smoother.
I have gotten simple queries to work however am unsure of how to implement one with this many joins. Many examples used sub queries but my attempts failed to return any records at all. I figured before I attempt filters i need to transcribe this query first.
videoController.php
public function actionIndex()
{
$sql = 'SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
FROM (((ispy.videos videos
INNER JOIN ispy.cameras cameras
ON (videos.cameras_idcameras = cameras.idcameras))
INNER JOIN ispy.host_machines host_machines
ON (cameras.host_machines_idhost_machines =
host_machines.idhost_machines))
INNER JOIN ispy.events events
ON (events.host_machines_idhost_machines =
host_machines.idhost_machines))
INNER JOIN ispy.staff staff
ON (events.staff_idreceptionist = staff.idreceptionist)
WHERE (staff.idreceptionist = 182)
AND (events.event_type IN (23, 24))
AND (events.event_timestamp BETWEEN videos.start_time
AND videos.end_time)';
$query = Videos::findBySql($sql);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
Failed Attempt
public function actionIndex()
{
$query = Videos::find()
->innerJoin('cameras', 'videos.cameras_idcameras = cameras.idcameras')
->innerJoin('host_machines', 'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('events', 'events.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('staff', 'events.staff_idreceptionist = staff.idreceptionist')
->where('staff.idreceptionist = 182')
->andWhere(['events.event_type' => [23,24]])
->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
Portion of View
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'event_type',
'event_timestamp',
'filelocation',
//['class' => 'yii\grid\ActionColumn'],
],
]); ?>
Please let me know if i need to be more specific or include any additional information.
Thanks ahead
i will assume, based on the question you asked here you liked in comments that you provided the entire query
(no other fields, that you took out just to show sample code)
therefore, if you only need only the fields specified in SELECT statement, you can optimize your query quite a bit:
first off, you're joining with host_machines only to link cameras and events, but have the same key host_machines_idhost_machines on both, so that's not needed, you can directly:
INNER JOIN events events
ON (events.host_machines_idhost_machines =
cameras.host_machines_idhost_machines))
secondly, the join with ispy.staff, the only used field is idreceptionist in WHERE clause, that field exists in events as well so we can drop it completly
the final query here:
SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
FROM videos videos
INNER JOIN cameras cameras
ON videos.cameras_idcameras = cameras.idcameras
INNER JOIN events events
ON events.host_machines_idhost_machines =
cameras.host_machines_idhost_machines
WHERE (events.staff_idreceptionist = 182)
AND (events.event_type IN (23, 24))
AND (events.event_timestamp BETWEEN videos.start_time
AND videos.end_time)
should output the same records as the one in your question, without any identitcal rows
some video duplicates will still exists due to one to many relation between cameras and events
now to the yii side of things,
you have to define some relations on the Videos model
// this is pretty straight forward, `videos`.`cameras_idcameras` links to a
// single camera (one-to-one)
public function getCamera(){
return $this->hasOne(Camera::className(), ['idcameras' => 'cameras_idcameras']);
}
// link the events table using `cameras` as a pivot table (one-to-many)
public function getEvents(){
return $this->hasMany(Event::className(), [
// host machine of event => host machine of camera (from via call)
'host_machines_idhost_machines' => 'host_machines_idhost_machines'
])->via('camera');
}
the VideoController and the search function itself
public function actionIndex() {
// this will be the query used to create the ActiveDataProvider
$query =Video::find()
->joinWith(['camera', 'events'], true, 'INNER JOIN')
->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
yii will treat each video as a single record (based on pk), that means that all video duplicates are
removed. you will have single videos, each with multiple events so you wont be able to use 'event_type'
and 'event_timestamp' in the view but you can declare some getters inside Video model to show that info:
public function getEventTypes(){
return implode(', ', ArrayHelper::getColumn($this->events, 'event_type'));
}
public function getEventTimestamps(){
return implode(', ', ArrayHelper::getColumn($this->events, 'event_timestamp'));
}
and the view use:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'eventTypes',
'eventTimestamps',
'filelocation',
//['class' => 'yii\grid\ActionColumn'],
],
]); ?>
edit:
if you want to keep the video duplicates, declare the two columns from events inside Video model
public $event_type, $event_timestamp;
keep the original GridView setup, and add a select and indexBy this to the query inside VideoController:
$q = Video::find()
// spcify fields
->addSelect(['videos.idvideo', 'videos.filelocation', 'events.event_type', 'events.event_timestamp'])
->joinWith(['camera', 'events'], true, 'INNER JOIN')
->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time')
// force yii to treat each row as distinct
->indexBy(function () {
static $count;
return ($count++);
});
update
a direct staff relation to Video is currently somewhat problematic since that is more than one table away from it.
there's an issue about it here
however, you add the staff table by linking it to the Event model,
public function getStaff() {
return $this->hasOne(Staff::className(), ['idreceptionist' => 'staff_idreceptionist']);
}
that will allow you to query like this:
->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')
Filtering will require some small updates on the controller, view and a SarchModel
here's a minimal implementation:
class VideoSearch extends Video
{
public $eventType;
public $eventTimestamp;
public $username;
public function rules() {
return array_merge(parent::rules(), [
[['eventType', 'eventTimestamp', 'username'], 'safe']
]);
}
public function search($params) {
// add/adjust only conditions that ALWAYS apply here:
$q = parent::find()
->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')
->where([
'event_type' => [23, 24],
// 'staff_idreceptionist' => 182
// im guessing this would be the username we want to filter by
])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');
$dataProvider = new ActiveDataProvider(['query' => $q]);
if (!$this->validate())
return $dataProvider;
$this->load($params);
$q->andFilterWhere([
'idvideo' => $this->idvideo,
'events.event_type' => $this->eventType,
'events.event_timestamp' => $this->eventTimestamp,
'staff.username' => $this->username,
]);
return $dataProvider;
}
}
controller:
public function actionIndex() {
$searchModel = new VideoSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('test', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
and the view
use yii\grid\GridView;
use yii\helpers\ArrayHelper;
echo GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'filelocation',
[
'attribute' => 'eventType', // from VideoSearch::$eventType (this is the one you filter by)
'value' => 'eventTypes' // from Video::getEventTypes() that i suggested yesterday
// in hindsight, this could have been named better, like Video::formatEventTypes or smth
],
[
'attribute' => 'eventTimestamp',
'value' => 'eventTimestamps'
],
[
'attribute' => 'username',
'value' => function($video){
return implode(', ', ArrayHelper::map($video->events, 'idevent', 'staff.username'));
}
],
//['class' => 'yii\grid\ActionColumn'],
],
]);
My recommendation would be to have 2 queries. The first one to get the ids of the videos that fit your search, the second query theone that uses those ids and feeds your $dataProvider.
use yii\helpers\ArrayHelper;
...
public function actionIndex()
{
// This is basically the same query you had before
$searchResults = Videos::find()
// change 'id' for the name of your primary key
->select('id')
// we don't really need ActiveRecord instances, better use array
->asArray()
->innerJoin('cameras', 'videos.cameras_idcameras = cameras.idcameras')
->innerJoin('host_machines', 'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('events', 'events.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('staff', 'events.staff_idreceptionist = staff.idreceptionist')
->where('staff.idreceptionist = 182')
->andWhere(['events.event_type' => [23,24]])
->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time'])
// query the results
->all();
// this will be the query used to create the ActiveDataProvider
$query = Videos::find()
// and we use the results of the previous query to filter this one
->where(['id' => ArrayHelper::getColumn($searchResults, 'id')]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
I would like to have a CRUD table, and to be specific I don't need to edit/delete records, only the part with filtering results, which appears at the top of the CRUD generated table, is the part that I want to have. My table contains data from 1 table in database, but I have one column that is not connected to this or any other table in database (it's a comment, that is auto-generated, based on one column from the table). I generate my table manually, but I'd like to add the part with filtering. I have no idea how to do it though. Is it possible in Yii2 to do it manually, or do I have to use CRUD generator?
I don't use CRUD generator as it doesn't generate the code I want (and I think it also doesn't generate filters?). I use a basic template which fits to nearly all gridviews I need to display. Here's one example that can give you a filter:
use yii\grid\GridView;
/** #var array $userTypes All user types (classes) */
// ...
echo GridView::widget([
'dataProvider' => $modelProvider,
'filterModel' => $model,
'columns' => [
[
'attribute' => 'id',
'format' => 'raw',
'filter' => Html::input('text', 'User[id]', $model->id, ['class' => 'form-control', 'placeholder' => 'Filter ID']),
[
'attribute' => 'type',
'format' => 'raw',
'filter' => Html::activeDropDownList($model, 'type', $userTypes, ['class' => 'form-control', 'prompt' => 'All types']),
],
]);
In here I use 2 different input field types (text and dropdown).
For Html::input, first is type (text), then full attribute name (model name + attribute name), then default value and finally other options.
For Html::activeDropDownList we have model first, then attribute name (only), that items list (array) and finally other options.
I guess you are talking about GridView, if yes then you can have your own columns in it, no problem. lets call that column comment as you mentioned
If you use basic template, generated by Gii, and you also generate search class for that model, then you comment to safe attributes and add code for that code to be able to filter it.
If you could be more detailed about mentioned column, possible values or algorythm you might get more suitable answers...
Also take look at Yii 2.0: Displaying, Sorting and Filtering Model Relations on a GridView
Lets say your model is called Xyz as you did not provided any. Also I named column from your table as column_from_your_table and your virtual column as comment
In your model Xyz you will add relation (method with specific name to define it)
public function getComment()
{
$column_from_your_table = $this->column_from_your_table;
$comment = '';
// your code to specify the relation
// ...
// this is value dislpayed in column grid
return $comment;
}
and in file XyzSearch.php in \app\models\
you will have something like this (edit to your needs ofcourse)
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use yii\db\Expression;
/**
* XyzSearch represents the model behind the search form about `app\models\Xyz`.
*/
class XyzSearch extends Xyz
{
public $comment; // your virtual column
/**
* #inheritdoc
*/
public function rules()
{
return [
// add it to safe attributes
[['comment'], 'safe'],
// you will have more rules for your other columns from DB probably
];
}
/**
* #inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search($params)
{
$query = Xyz::find();
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
// I dont know your how it is your comment column autogenerated
// or what is the relation, so I give you just basic idea
// your algorythm needs to be able to be rewritten in SQL
// otherwise I dont know how you could possibly sort it
$dataProvider->sort->attributes['comment'] = [
'asc' => [Xyz::tableName().'.column_from_your_table' => SORT_ASC],
'desc' => [Xyz::tableName().'.column_from_your_table' => SORT_DESC],
'default' => SORT_ASC,
];
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// again, you will have more filtering conditions from your generated code
// then you will add your custom filtering condition
// I dont know your how it is your comment column autogenerated
// or what is the relation, so I give you just basic idea
$query->andFilterWhere(['like', Xyz::tableName().'.column_from_your_table', $this->comment]);
return $dataProvider;
}
}
finaly in your view file add your virutal column
<?php echo GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
// other columns from database
// ...
'comment',
['class' => 'yii\grid\ActionColumn'],
]
]); ?>
I'm working on Yii2 Advance Application. I've two tables user (default yii2 db schema table) and user_profile(newly added). I've created relation between them by adding user_profile_id in user table and also made change in their respective models.
Following code added in User Model:
/**
* #return \yii\db\ActiveQuery
*/
public function getUserProfile() {
return $this->hasOne(UserProfile::className(), ['id' => 'user_profile_id']);
}
And in UserProfile Model
/**
* #return \yii\db\ActiveQuery
*/
public function getUsers()
{
return $this->hasMany(User::className(), ['user_profile_id' => 'id']);
}
Now, when I'm trying to pull data of UserProfile model from User gridview its giving me error:
User Gridview:
<?=
GridView::widget([
'dataProvider' => $dataProvider,
//'filterModel' => $searchModel,
'columns' => [
'userProfile.first_name',
[
'class' => 'yii\grid\ActionColumn',
'header' => 'Action',
'template' => ' {update} {delete}',
],
],
]);
?>
(see userProfile.first_name)
Error:
Unknown Class – yii\base\UnknownClassException
Unable to find 'backend\models\UserProfile' in file: D:\www\pcwf/backend/models/UserProfile.php. Namespace missing?
Where I'm going wrong. Any help would be appreciated.