I'm going to use 1st normalization form in my Yii2 project, so I've added table like this | id | post_id | tag_id |and when I'm to posts model I've writed this:
public function getTags()
{
return $this->hasMany(PostTags::className(), ['post_id' => 'id']);
}
In view widget I've added 'tags.tag_id', but it shows no data.
Is there any way to show this tags in DetailView and GridView widgets?
May be, I can write "group_concat" somewhere?
I'd recommend to write a widget for displaying a links list of related records. It's reusable, prevents HTML generation in model / controller, reduces the amount of code in view.
<?php
namespace common\widgets;
use yii\base\Widget;
use yii\helpers\Html;
/**
* Widget for display list of links to related models
*/
class RelatedList extends Widget
{
/**
* #var \yii\db\ActiveRecord[] Related models
*/
public $models = [];
/**
* #var string Base to build text content of the link.
* You should specify attribute name. In case of dynamic generation ('getFullName()') you should specify just 'fullName'.
*/
public $linkContentBase = 'name';
/**
* #var string Route to build url to related model
*/
public $viewRoute;
/**
* #inheritdoc
*/
public function run()
{
if (!$this->models) {
return null;
}
$items = [];
foreach ($this->models as $model) {
$items[] = Html::a($model->{$this->linkContentBase}, [$this->viewRoute, 'id' => $model->id]);
}
return Html::ul($items, [
'class' => 'list-unstyled',
'encode' => false,
]);
}
}
Here are some examples (assuming tag name is stored in name column).
Usage in GridView:
[
'attribute' => 'tags',
'format' => 'raw',
'value' => function ($model) {
/* #var $model common\models\Post */
return RelatedList::widget([
'models' => $model->tags,
'viewRoute' => '/tags/view',
]);
},
],
Usage in DetailView:
/* #var $model common\models\Post */
...
[
'attribute' => 'tags',
'format' => 'raw',
'value' => RelatedList::widget([
'models' => $model->tags,
'viewRoute' => '/tags/view',
]),
],
Don't forget to set format raw, because by default content is rendered as plain text to prevent XSS attacks (html special characters are escaped).
You can modify this to fit your needs, this is just an example.
Related
I'm using Sonata admin in my Symfony project. I have 2 entities like Parent and Child. Parent entity is connected to child by one-to-many relationship.
I have created 2 admin classes for child entity with different baseRoutName. I need to use Child entity fields in Parent entity sonata form for 2 times.
//ParentAdmin.php
$formMapper
->with('Child 1', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child1'
])
->end()
->with('Child 2', ['class' => 'col-md-4'])
->add('child', CollectionType::class, [], [
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
'admin_code' => 'admin.child2'
])
->end();
The problem here is that I need to use child field for multiple times. But the child field within Child 2 is overriding the child field in Child 1. As you can see I have used different admin_code for these 2 fields.
My expected output is,
But the actual output I'm getting is,
I know the problem here is duplicate entity fields. Is it possible to display same field for multiple times?
Does anyone have solution/suggestion? Thanks in advance!!
maybe it's late but i had the same problem, found this post with no answer, and finally found a solution, so here it is for future purpose.
i have a Request class with generated document, get an eye on get and add functions:
class Request
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="GeneratedDocument", mappedBy="request", cascade={"all"}, orphanRemoval=true)
*/
protected $generatedDocuments;
/**
* #var ArrayCollection
* each of this attributes will get one type of $generatedDocuments
*/
protected $generatedAttestationDocuments;
protected $generatedCertificationDocuments;
public function __construct()
{
$this->generatedDocuments = new ArrayCollection();
}
/**
* Set the value of id.
*
* #param integer $id
* #return \App\Entity\Request
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get the value of id.
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedDocuments(): Collection
{
return $this->generatedDocuments;
}
public function addGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if (!$this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments[] = $generatedDocument;
$generatedDocument->setRequest($this);
}
return $this;
}
public function removeGeneratedDocument(GeneratedDocument $generatedDocument): self
{
if ($this->generatedDocuments->contains($generatedDocument)) {
$this->generatedDocuments->removeElement($generatedDocument);
// set the owning side to null (unless already changed)
if ($generatedDocument->getRequest() === $this) {
$generatedDocument->setRequest(null);
}
}
return $this;
}
/**
* #return Collection|GeneratedDocument[]
*
* #param int $type
*/
protected function getTypedGeneratedDocuments(int $type): Collection
{
return $this->getGeneratedDocuments()->filter(function (GeneratedDocument $gd) use ($type) {
if ($gd->getGeneratedDocumentModel()) {
return $type === $gd->getGeneratedDocumentModel()->getType();
}
return false;
});
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedAttestationDocuments(): Collection
{
if (empty($this->generatedAttestationDocuments)) {
$this->generatedAttestationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_ATTESTATION);
}
return $this->generatedAttestationDocuments;
}
public function addGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedAttestationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedAttestationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
/**
* #return Collection|GeneratedDocument[]
*/
public function getGeneratedCertificationDocuments(): Collection
{
if (empty($this->generatedCertificationDocuments)) {
$this->generatedCertificationDocuments =
$this->getTypedGeneratedDocuments(GeneratedDocumentModel::TYPE_CERTIFICATE);
}
return $this->generatedCertificationDocuments;
}
public function addGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
$this->generatedCertificationDocuments[] = $generatedDocument;
return $this->addGeneratedDocument($generatedDocument);
}
public function removeGeneratedCertificationDocument(GeneratedDocument $generatedDocument): self
{
return $this->removeGeneratedDocument($generatedDocument);
}
}
Then in admin you have to give a different name in each add, here I use my typed generated documents so there's no duplication problem, but they are not mapped and symfony complain about it.
Trying to use 'mapped' => false resolved nothing, the simplest way I found was a 'virtual mapping', based on the original mapped attribute 'generatedDocuments, just the time to fool symfony when building the form.
class RequestAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper): void
{
/** #var Request $createdRequest */
$createdRequest = $this->getSubject();
$metaData = $this->getModelManager()->getMetadata($this->getClass());
//We need many CollectionType based on 'generatedDocuments', and we need an ArrayCollection for each of them
//so here is a virtual mapping to make symfony accept the persistence of our CollectionType
//then setter should fill 'generatedDocuments'
$mapping = $metaData->getAssociationMappings()['generatedDocuments'];
$mapping['fieldName'] = 'generatedAttestationDocuments';
$metaData->mapOneToMany($mapping);
$mapping['fieldName'] = 'generatedCertificationDocuments';
$metaData->mapOneToMany($mapping);
$formMapper
->with(('Attestation Deposit'))
->add('generatedAttestationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Attestation',
'data' => $createdRequest->getGeneratedAttestationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_attestation',
])
->end()
->with(('Certificate'))
->add('generatedCertificationDocuments', CollectionType::class, [
'label' => false,
'by_reference' => false,
'btn_add' => 'Add Certification',
'data' => $createdRequest->getGeneratedCertificationDocuments(),
], [
'edit' => 'inline',
'inline' => 'table',
'admin_code' => 'admin.generated_document_certification',
])
->end()
//delete virtual mapping to avoid it to get handle like a real mapping
unset($metaData->associationMappings['generatedAttestationDocuments']);
unset($metaData->associationMappings['generatedCertificationDocuments']);
}
}
I would like to know a simplest way, but It really work like individual CollectionType for me.
Hope it will help!
I need help with search model for ArrayDataProvider. Let's say i have an array:
$cities = [
['city' => "Chicago", 'year' => 1984],
['city' => "Washington", 'year' => 2001],
['city' => Manchester", 'year' => 1997],
//and so on...
];
I create an ArrayDataProvider:
$provider = new \yii\data\ArrayDataProvider([
'allModels' => $catalog,
'sort' => [
'attributes' => ['city', 'year'],
],
]);
Then I create a GridView:
echo \yii\grid\GridView::widget([
'dataProvider' => $provider,
'filterModel' => (new LibrarySearchModel()),
'columns' => $columns,
'showHeader' => true,
'summary' => false,
]);
All works fine, but i need a filtering in GridView. There is no option to use ActiveDataProvider and I cant find any tutorial how to filter a data in ArrayDataProvider.
Can someone help me with code for filter model or recomend the docs for my case?
This is example of how to use ArrayDataProvider with filters in the GridView.
Let's create simple action.
public function actionExample()
{
$data = new \app\models\Data();
$provider = $data->search(Yii::$app->request->get());
return $this->render('example', [
'provider' => $provider,
'filter' => $data,
]);
}
This is classic Yii 2 approach to the GridView so I will not explain it (you can find details in the Guide linked above).
Now the view.
<?php
echo \yii\grid\GridView::widget([
'dataProvider' => $provider,
'filterModel' => $filter,
'columns' => [
'name',
'code',
],
]);
Again, nothing different from the ActiveDataProvider approach. As you can see here we are expecting two columns: name and code - these will be defined below.
Data model.
Prepare the model that will handle the data source. Explanation is given in the comments.
<?php
namespace app\models;
use yii\base\Model;
/**
* Our data model extends yii\base\Model class so we can get easy to use and yet
* powerful Yii 2 validation mechanism.
*/
class Data extends Model
{
/**
* We plan to get two columns in our grid that can be filtered.
* Add more if required. You don't have to add all of them.
*/
public $name;
public $code;
/**
* Here we can define validation rules for each filtered column.
* See http://www.yiiframework.com/doc-2.0/guide-input-validation.html
* for more information about validation.
*/
public function rules()
{
return [
[['name', 'code'], 'string'],
// our columns are just simple string, nothing fancy
];
}
/**
* In this example we keep this special property to know if columns should be
* filtered or not. See search() method below.
*/
private $_filtered = false;
/**
* This method returns ArrayDataProvider.
* Filtered and sorted if required.
*/
public function search($params)
{
/**
* $params is the array of GET parameters passed in the actionExample().
* These are being loaded and validated.
* If validation is successful _filtered property is set to true to prepare
* data source. If not - data source is displayed without any filtering.
*/
if ($this->load($params) && $this->validate()) {
$this->_filtered = true;
}
return new \yii\data\ArrayDataProvider([
// ArrayDataProvider here takes the actual data source
'allModels' => $this->getData(),
'sort' => [
// we want our columns to be sortable:
'attributes' => ['name', 'code'],
],
]);
}
/**
* Here we are preparing the data source and applying the filters
* if _filtered property is set to true.
*/
protected function getData()
{
$data = [
['name' => 'Paul', 'code' => 'abc'],
['name' => 'John', 'code' => 'ade'],
['name' => 'Rick', 'code' => 'dbn'],
];
if ($this->_filtered) {
$data = array_filter($data, function ($value) {
$conditions = [true];
if (!empty($this->name)) {
$conditions[] = strpos($value['name'], $this->name) !== false;
}
if (!empty($this->code)) {
$conditions[] = strpos($value['code'], $this->code) !== false;
}
return array_product($conditions);
});
}
return $data;
}
}
The filtering in this example is handled by the array_filter function. Both columns are filtered "database LIKE"-style - if column value contains the searched string the data array row is not removed from the source.
To make it work like and conditions in ActiveDataProvider we put boolean result of every column check in the $conditions array and return product of that array in array_filter.
array_product($conditions) is equivalent of writing $conditions[0] && $conditions[1] && $conditions[2] && ...
This all results in the filterable and sortable GridView widget with two columns.
in index.php :
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'ID_REQUEST',
'NOMOR_SURAT',
[
'label' => 'Nama Depan',
'attribute' => 'ID_KARYAWAN',
'value' => 'iDKARYAWAN.FIRST_NAME'
],
[
'label' => 'Nama Belakang',
'attribute' => 'ID_KARYAWAN',
'value' => 'iDKARYAWAN.LAST_NAME'
],
which is iDKARYAWAN is relation from another table in my model
class Request extends \yii\db\ActiveRecord {
/**
* #inheritdoc
*/
public static function tableName() {
return 'ytms_it.request';
}
public function getIDKARYAWAN() {
return $this->hasOne(Karyawan::className(), ['ID_KARYAWAN' => 'ID_KARYAWAN'])->from(Karyawan::tableName(). ' b');
}
How to merge those two column ?
For the elp, thanks.
Create method called getFullName() in related model and calculate full name using PHP concatenation:
use yii\helpers\Html;
...
/**
* #return string
*/
public function getFullName()
{
return Html::encode($this->name . ' ' . $this->surname);
}
Optionally define a label for it in attributeLabels() method of related model:
`fullName` => 'Label for full name',
Then in GridView it's possible to display full name of related model in one column like so:
1) The shortest form:
'relatedModel.fullName',
2) Overriding the label:
[
'attribute' => 'relatedModel.fullName',
'label' => 'Overrided label',
],
3) Using closure:
[
'attribute' => 'relatedModel.fullName', // The attribute name can be different in this case
'value' => function ($model) {
// You can calculate full name here.
// But it's better to just call according method since view is only for display.
return $model->author->fullName;
},
],
Another way is to calculate full name using SQL and include as a part of query result in separate column.
Use Active Record - Selecting extra fields official docs section as a guide, also see this related issue on Github - JoinWith - assign a column aliases to an attribute of related model.
Add $fullName as public property of related model class. Modify query like so:
use yii\db\Expression;
...
->joinWith(['relatedModel' => function (\yii\db\ActiveQuery $query) {
$query->addSelect('fullName' => new Expression("CONCAT(name, ' ', surname)")]);
}]
Then to display it in GridView column you can use one of the options desribed above, for example:
'relatedModel.fullName'
I am trying to get information from two models that are related, displayed in one view.
So what I am trying to accomplish is have the index view to show the list of people, if I then go into detail view of that particular person I want a list of attributes relevant to that person to appear.
I have the database setup so that when I create a new person a default row gets inserted into the attributes table with the id of the person under the column called person_id.
See my two model classes
People:
class People extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public static function tableName()
{
return 'people';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['dob', 'CURDATE'], 'safe'],
[['age'], 'integer'],
[['firstname', 'surname'], 'string', 'max' => 50]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'firstname' => 'Firstname',
'surname' => 'Surname',
'dob' => 'Dob',
'age' => 'Age',
'CURDATE' => 'Curdate',
];
}
/**
* #return \yii\db\ActiveQuery
*/
public function getId0()
{
return $this->hasOne(Attributes::className(), ['person_id' => 'id']);
}
}
Attributes:
class Attributes extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public static function tableName()
{
return 'attributes';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['haircolor', 'eyecolor', 'weight', 'height', 'person_id'], 'required'],
[['weight', 'height', 'person_id'], 'integer'],
[['haircolor', 'eyecolor'], 'string', 'max' => 50]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'haircolor' => 'Haircolor',
'eyecolor' => 'Eyecolor',
'weight' => 'Weight',
'height' => 'Height',
'person_id' => 'Person ID',
];
}
/**
* #return \yii\db\ActiveQuery
*/
public function getPeople()
{
return $this->hasOne(People::className(), ['id' => 'person_id']);
}
}
I have generated CRUD through Gii for both of these models.
What I would like to know is how to setup my people controller and people view so that this may work properly.
Just to recap, my index.php view will just show the list of people, if a record exists, you can view that specific record, if you view the record - which will be the view.php file, I want to show the attributes(These will be the default values) of that particular person where the id of the person is the same as the person_id in the attributes table
The user will then be able to update the attributes relating to that person.
Kind Regards.
Here an example :
public function actionCreate()
{
$user = new User;
$profile = new Profile;
if ($user->load(Yii::$app->request->post()) && $profile->load(Yii::$app->request->post()) && Model::validateMultiple([$user, $profile])) {
$user->save(false); // skip validation as model is already validated
$profile->user_id = $user->id; // no need for validation rule on user_id as you set it yourself
$profile-save(false);
return $this->redirect(['view', 'id' => $user->id]);
} else {
return $this->render('create', [
'user' => $user,
'profile' => $profile,
]);
}
}
More informations :
http://www.yiiframework.com/forum/index.php/topic/53935-solved-subforms/page__p__248184
http://www.yiiframework.com/doc-2.0/guide-input-tabular-input.html
To display related information in a view, you get the best performance with eager loading. I'll provide an example:
public function actionView($id)
{
$model = Person::find()
->where(['id' => $id])
->with('id0')
->one();
return $this->render('view', [
'model' => $model,
]);
}
Now i see that your relation in Person Model is called getId0, you can for readability change that to getAttribs(), and change to ->with('attribs') but that is just a digression :)
EDIT: as #soju commented, attributes is not possible to use as a relation name, and that is why gii has given it the name getId0. Attribs or something more informative can be helpful on readability.
If you want to show the results in a widget, like GridView or ListView, you can follow the guide here:
http://www.ramirezcobos.com/2014/04/16/displaying-sorting-and-filtering-model-relations-on-a-gridview-yii2/
EDIT2: as #soju commented, guide is possibly outdated. Read official documents aswell.
http://www.yiiframework.com/doc-2.0/guide-output-data-widgets.html#working-with-model-relations
If you want to create your own view, you can access the values with $model->id0->haircolor or, if you rename the relation, $model->attribs->haircolor just like you would any other attribute.
Remember: using GridView / ListView requires the table name from the db when displaying, like 'attributes.eyecolor', but the $model->id0 requires the relation name from the model, without the 'get' in front, and with lower case.
I am trying to setup the filter for related model in Yii2's GridView widget, but I am keep getting the error like the filter value must be an integer.
I have followed this question. Now, I have a two models Services.php and ServiceCharge.php.
In ServiceCharge.php the relation is setup like:
public function getServiceName()
{
return $this->hasOne(Services::className(),['id'=>'service_name']);
}
In the ServiceChargeSearch.php the code is like this:
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\ServiceCharges;
/**
* ServiceChargesSearch represents the model behind the search form about `app\models\ServiceCharges`.
*/
class ServiceChargesSearch extends ServiceCharges
{
/**
* #inheritdoc
*/
public function attributes()
{
// add related fields to searchable attributes
return array_merge(parent::attributes(), ['serviceName.services']);
}
public function rules()
{
return [
[['id'], 'integer'],
[['charges_cash', 'charges_cashless'], 'number'],
[['id', 'serviceName.services', 'room_category'], 'safe'],
];
}
/**
* #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 = ServiceCharges::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['serviceName.services'] = [
'asc' => ['serviceName.services' => SORT_ASC],
'desc' => ['serviceName.services' => SORT_DESC],
];
$query->joinWith(['serviceName']);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
$query->andFilterWhere([
'id' => $this->id,
// 'service_name' => $this->service_name,
'room_category' => $this->room_category,
'charges_cash' => $this->charges_cash,
'charges_cashless' => $this->charges_cashless,
])
->andFilterWhere(['LIKE', 'serviceName.services', $this->getAttribute('serviceName.services')]);
return $dataProvider;
}
}
and in my Gridview it is setup like this:
[
'attribute'=>'service_name',
'value'=>'serviceName.services',
],
Which is showing the services name from the related model correctly.
I am not able to see what I am doing wrong, but the filter field for the attribute for service is not showing at all.
Actually it is much simpler than it seems.
add the column_name to safe attribute.
Note: this should be relation Name
add the join with query - like - $query->joinWith(['serviceName','roomCategory']);
add the filter condition like:
->andFilterWhere(['like', 'services.services', $this->service_name])
->andFilterWhere(['like', 'room_category.room_category', $this->room_category]);
if like to add sorting add the code like:
$dataProvider->sort->attributes['service_name'] = [
'asc' => ['services.services' => SORT_ASC],
'desc' => ['services.services' => SORT_DESC],
];
$dataProvider->sort->attributes['room_category'] = [
'asc' => ['room_category.room_category' => SORT_ASC],
'desc' => ['room_category.room_category' => SORT_DESC],
];
5 you should also set the relation name say public $roomCategory
That's it. Both sorting and filtering for related table works perfectly.
Note: Remove default validation like integer for related column and default filtering generated by gii otherwise it will generate an error.
Update on Latest version:
Adding Public $attribute is not needed.
Adding safe attribute for relation is also not needed.
but the attribute in your current model, which you want filter is
to added to safe attribute that is a must.
and most importantly in your gridview, the related attribute has to
be in closure format.
that is example
[
'attribute=>'attribute_name',
'value=function($data){
return $data->relationname->related_table_attribute_name
}
],
remember it you are using relation_name.related_table_attribute_name filter somehow doesn't work for me.
There is a fairly comprehensive set of instructions on the Yii Framework website. The only thing to note is that the search model complains about the following lines, but everything appears to work as intended without them:
$this->addCondition(...);
For a model, PaymentEvent (table: subs_payment_event), which has a currency_id field linked to model Currency, this is the complete set of additional code (using the Basic template):
In the main model, PaymentEvent.php:
public function getCurrencyName()
{
return $this->currency->name;
}
In the search model, PaymentEventSearch.php:
public $currencyName;
In its rules:
[['currencyName'], 'safe'],
In the attributes of its setSort statement, include:
'currencyName' => [
'asc' => ['subs_currency.name' => SORT_ASC],
'desc' => ['subs_currency.name' => SORT_DESC],
'label' => 'Currency'
],
Before the grid filtering conditions:
$query->joinWith(['currency' => function ($q) {
$q->where('subs_currency.name LIKE "%' . $this->currencyName . '%"');
}]);
Finally, in the GridView columns array in the view (including my usual link across to the related model records):
[
'attribute' => 'currencyName',
'label' => 'Currency',
'format' => 'raw',
'value' => function ($data) {
return Html::a($data->currency->name, ['/currency/' . $data->currency_id]);
},
],