Several identical relations to one model in Yii2 - yii2

I have a model File to store uploaded files and information about these files. Also there is a model with Company relations logo hasOne(File::className()) and photos hasMany(File::className()). Relations are written and works fine. Now I need to make an edit form for model Company in which I could edit files associated in logo and photos. Please tell me how I can do it.

Your relations can reflect the different use-cases, so in your Company model you can have
public function getLogo(){
//You'll need to add in the other attributes that define how Yii is to retrieve the logo from your images
return $this->hasOne(File::className(), ['companyId' => 'id', 'isLogo' => true]);
}
public function getPhotos(){
//You'll need to add in the other attributes that define how Yii is to retrieve the photos from your images
return $this->hasMany(File::className(), ['companyId' => 'id', 'isLogo' => false]);
}
You can then just use them like normal attributes;
$company = new Company;
$logo = $company->logo;
$photos = $company->photos;
You will then need to set up your controller to handle changes in these values to deal with uploads or new images, but that will depend on how you are handling the uploads.

Related

How to implement Filtering on YII restful GET api?

I am working on Restful APIs of Yii.
My controller name is ProductsController and the Model is Product.
When I call API like this GET /products, I got the listing of all the products.
But, now I want to filter the records inside the listing API.
For Example, I only want those records Which are having a product name as chairs.
How to implement this?
How to apply proper filtering on my Rest API. I am new to this. So, I have no idea how to implement this. I also followed their documentation but unable to understand.
May someone please suggest me a good example or a way to achieve this?
First of all you need to have validation rules in your model as usual.
Then it's the controllers job and depending on the chosen implementation I can give you some hints:
If your ProductsController extends yii\rest\ActiveController
Basically the easiest way because almost everything is already prepared for you. You just need to provide the $modelClass there and tweak actions() method a bit.
public function actions()
{
$actions = parent::actions();
$actions['index']['dataFilter'] = [
'class' => \yii\data\ActiveDataFilter::class,
'searchModel' => $this->modelClass,
];
return $actions;
}
Here we are modifying the configuration for IndexAction which is by default responsible for GET /products request handling. The configuration is defined here and we want to just add dataFilter key configured to use ActiveDataFilter which processes filter query on the searched model which is our Product. The other actions are remaining the same.
Now you can use DataProvider filters like this (assuming that property storing the product's name is name):
GET /products?filter[name]=chairs will return list of all Products where name is chairs,
GET /products?filter[name][like]=chairs will return list of all Products where name contains word chairs.
If your ProductsController doesn't extend yii\rest\ActiveController but you are still using DataProvider to get collection
Hopefully your ProductsController extends yii\rest\Controller because it will already benefit from serializer and other utilities but it's not required.
The solution is the same as above but now you have to add it by yourself so make sure your controller's action contains something like this:
$requestParams = \Yii::$app->getRequest()->getBodyParams(); // [1]
if (empty($requestParams)) {
$requestParams = \Yii::$app->getRequest()->getQueryParams(); // [2]
}
$dataFilter = new \yii\data\ActiveDataFilter([
'searchModel' => Product::class // [3]
]);
if ($dataFilter->load($requestParams)) {
$filter = $dataFilter->build(); // [4]
if ($filter === false) { // [5]
return $dataFilter;
}
}
$query = Product::find();
if (!empty($filter)) {
$query->andWhere($filter); // [6]
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]); // [7]
What is going on here (numbers matching the code comments):
We are gathering request parameters from the body,
If these are empty we take them from the URL,
We are preparing ActiveDataFilter as mentioned above with searched model being the Product,
ActiveDataFilter object is built using the gathered parameters,
If the build process returns false it means there is an error (usually unsuccessful validation) so we return the object to user to see list of errors,
If the filter is not empty we are applying it to the database query for Product,
Finally we are configuring ActiveDataProvider object to return the filtered (and paginated and sorted if applicable) collection.
Now you can use DataProvider filters just as mentioned above.
If your ProductsController doesn't use DataProvider to get collection
You need to create your custom solution.

Yii2: db access from widget

Widgets should be designed to be self-contained and they should not access a database directly. But recently I've come across the code that uses a direct access to the database to retrieve widget settings, and also caches retrieved values.
Here's the part of the widget:
class DbCarousel extends Carousel
{
// ...
public function init()
{
$cacheKey = [
WidgetCarousel::className(),
$this->key
];
$items = Yii::$app->cache->get($cacheKey);
if ($items === false) {
$items = [];
$query = WidgetCarouselItem::find()
->joinWith('carousel')
->where([
'{{%widget_carousel_item}}.status' => 1,
'{{%widget_carousel}}.status' => WidgetCarousel::STATUS_ACTIVE,
'{{%widget_carousel}}.key' => $this->key,
])
->orderBy(['order' => SORT_ASC]);
foreach ($query->all() as $k => $item) {
/** #var $item \common\models\WidgetCarouselItem */
if ($item->path) {
$items[$k]['content'] = Html::img($item->getImageUrl());
}
if ($item->caption) {
$items[$k]['caption'] = $item->caption;
}
}
Yii::$app->cache->set($cacheKey, $items, 60*60*24*365);
}
$this->items = $items;
parent::init();
}
// ...
}
Question is: can widget under any circumstances access a database, or it's a sign that refactoring is needed?
Technically querying data and representing it are two different tasks, so such widget breaks single responsibility principle. However Yii has already a abstraction for retrieving data from database (ActiveRecord and/or ActiveQuery), so it is not that simple. You don't need to create separate data provider class for calling News::find()->newestFirst()->limit(5)->all() - usually it is easier and more pragmatic to call this query directly in LatestNewsWidget.
In general you have three situations with widgets:
Widget may display different sets of data in the same way (like GridView).
The same data may be displayed in different ways by different widgets, like widget for displaying menu - there is one menu stored in database, but depending on layout different widget should be used.
Both, way of displaying and data, is unique - for example you have only one menu and only one layout (so there is only one widget for displaying menu).
As far as two first cases clearly indicates that data source and widget should be separated, it is not that clear in third case. Personally I'm often using widgets which are responsible for retrieving all necessary data and dependencies, so I can use them simply by MyWidget::widget(). I never had any problems with this, but I'm trying to avoid too complicated DB queries - complicity usually should be hidden behind ActiveQuery abstraction. Also you always need to be prepared for refactoring and extracting querying data to separate component - at some point your unique widget with unique set of data may become not unique and separating widget and data provider may be the only sane way to keep your code DRY.

yii2 getting data from two models and presenting it in view

I have two models users and pictures
To draw all pictures in view I have to get string from pictures, one of data is userID, then get string from users according to that userID, so I get userFolder
Then I can draw picture using combined data.
And this must be done for all pictures in DB.
I can start from controller
$picturesModel= Pictures::find()->all();
But then I have to do what? run a loop while which get users data, and then get complete data for drawing a picture and store it in some new array which after loop finish I should pass to view? Is this the best way ? or there is anything simple ?
Assuming that your Pictures has a relation one to one with Users
the you can add to you Pictures model a function
class Pictures extends ActiveRecord
{
// ...
public function getUser()
{
return $this->hasOne(Users::className(), ['id' => 'user_id']);
}
}
then if you controller or in your view you need accessing to the user related to the picture you can
$pictureModel = Pictures::find()->where(['id'=>123])->one();
You can access to the user related
$userModel= $pictureModel->User;
or for a collection of pictures
$picturesModels= Pictures::find()->all();
$userModel = $picturesModels[0]->user
or for the loop
$picturesModels= Pictures::find()->all();
foreach( $picturesModels as $key => $value ) {
echo $value->user->your_att;
}
you can take a look at http://www.yiiframework.com/doc-2.0/guide-db-active-record.html and http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#relational-data

Foreign keys and uploading a photo in yii 1.1

i did these two tasks separately but now i am not being able to use both of these tasks at a single place.
Problem statement:
I have a table name Business_items having foreign keys of table business and items. In model class here is the relation function.
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'business' => array(self::BELONGS_TO, 'Business', 'business_id'),
'items' => array(self::BELONGS_TO, 'Items', 'items_id'),
'itemReviews' => array(self::HAS_MANY, 'ItemReview', 'business_items_id'),
);
}
ok, in create business page, i have two fields, business name, items name and a third thing which is upload image. Both of the fields are searchable drop downs. I am taking business name and items name with the help of foreign keys. so i can see the values inside my business_items which were used to be keys. i did this by changing this code.
public function actionCreate()
{
$model=new PackageItems;
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($model);
if(isset($_POST['PackageItems']))
{
$temp=$model->items_id=$_POST['PackageItems']['items_id']; //items_id is a multiple list field
foreach($temp as $t)
{
$model->unsetAttributes();
$model->setIsNewRecord(true);
$model->package_id=$_POST['PackageItems']['package_id']; //package_id is a repeated field
$model->items_id=$t;
$model->insert();
}
if($model->save())
$this->redirect(array('admin','id'=>$model->id));
}
$this->render('create',array(
'model'=>$model,
));
}
so what i wanted i actually accomplished which was
id----items----package
1------cake-----buy one get one free
2----- pastry-----buy one get one free
second part:
I know how to upload image in yii, i followed this link and it worked http://www.yiiframework.com/wiki/349/how-to-upload-image-photo-and-path-entry-in-database-with-update-functionality/
separately now the problem is i want something like that
id----items----package-----------------------image
1------cake-----buy one get one free------1.jpg
2----- pastry-----buy one get one free------1.jpg
but the problem is
public function actionCreate()
{
$model=new Banner; // this is my model related to table
if(isset($_POST['Banner']))
{
$rnd = rand(0,9999); // generate random number between 0-9999
$model->attributes=$_POST['Banner'];
$uploadedFile=CUploadedFile::getInstance($model,'image');
$fileName = "{$rnd}-{$uploadedFile}"; // random number + file name
$model->image = $fileName;
if($model->save())
{
$uploadedFile->saveAs(Yii::app()->basePath.'/../banner/'.$fileName); // image will uplode to rootDirectory/banner/
$this->redirect(array('admin'));
}
}
$this->render('create',array(
'model'=>$model,
));
}
how can i use both of these codes (getting value using foreign key code and picture uploading code) i want to upload pictures as well as get the value from some other table using foreign key with my code.
I know its complicated but i need help.
Thanks and sorry in advance.
I'm not sure if I get your problem right, but as far as I understood, you want to upload a file and insert the name of this file in one of your database tables. If that's the case, a solution could be as follows:
First, add a new field to your database table in which you will store the file name, also add it to your ActiveRecord class and to your view.
Then add your code to save your related records (I think your foreach loop is for that).
Next add a the necessary code to upload the image. In the code to upload the image you can see that Yii will treat the file field like a normal text field, in which you will store the uploaded file name.
Finally you should save your model and, if it succeded, then proceed to save the file in the server.
Hope this helps.
UPDATE
I'll put some code in order to clarify my answer.
You say that the first part works for you, then I'll begin with that.
Your model PackageItems needs a new field, let it be image.
Next, I'm assuming that the user filled the form, so I'll skip the if and the render parts
$temp=$model->items_id=$_POST['PackageItems']['items_id'];
$uploadedFile=CUploadedFile::getInstance($model,'image');//get uploaded image info
$rnd = rand(0,9999);
$fileName = "{$rnd}-{$uploadedFile}"; // random number + file name
$model->image = $fileName;//store the new file name in the model
foreach($temp as $t){
$model->unsetAttributes();
$model->setIsNewRecord(true);
$model->package_id=$_POST['PackageItems']['package_id'];
$model->items_id=$t;
$model->insert();
}
if($model->save()){
$uploadedFile->saveAs(Yii::app()->basePath.'/../yourPath/'.$fileName);//if the record was saved in the database, then proceed to save the image in the server
$this->redirect(array('admin','id'=>$model->id));
}
If you want to upload multiple files check this link Yii 1.1: Uploading multiple images with CMultiFileUpload

Yii model: Dynamic table relations

Table.linkedIndex is related to LinkedIndex.ID. The value of the field LinkedIndex.TableName is either Linked1 or Linked2 and defines which of these tables is related to a row in Table.
Now i want to make a dynamical link with Yii models so that i can easily get from a Table row to the corresponding Linked1 or Linked2 row:
Table.linkedID = [LinkedIndex.TableName].ID
Example
Table values:
LinkedIndex values:
Now I should get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;
Model
In the model Table, I tried to make the relation to the table with the name of the value of linkedIndex.TableName:
public function relations()
{
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
'linked' => array(
self::HAS_ONE,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
)
}
But then I get the error:
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
Is there any way to make a dynamic relation Table.linkedID -> [LinkedIndex.TableName].ID with Yii Models?
Per the Yii docs here:
http://www.yiiframework.com/doc/api/1.1/CActiveRecord#relations-detail
I'd suggest using self::HAS_ONE instead (unless there can be multiple rows in LinkedIndex with the same ID - although from the looks of above, I doubt that's the case).
You can link tables together that have different keys by following the schema:
foreign_key => primary_key
In case you need to specify custom PK->FK association you can define it as array('fk'=>'pk'). For composite keys it will be array('fk_c1'=>'pk_с1','fk_c2'=>'pk_c2').
so in your case:
public function relations(){
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
);
}
where LinkedIndex is the class name for the LinkedIndex model (relative to your Table model - i.e. same folder. You could change that, of course) and array('ID' => 'linkedIndex') specifies the relationship as LinkedIndex.ID = Table.linkedIndex.
Edit
Looking at your updated example, I think you're misunderstanding how the relations function works. You're getting the error
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
because you're trying to create another relation here:
'linked' => array(
self::BELONGS_TO,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
This part: linkedIndex.TableName refers to a new model class linkedIndex.TableName, so Yii attempts to load that class' file linkedIndex.TableName.php and throws an error since it doesn't exist.
I think what you're looking for is to be able to access the value TableName within the table LinkedIndex, correct? If so, that's accessible from within the Table model via:
$this->linkedIndex->TableName
This is made possible by the relation we set up above. $this refers to the Table model, linkedIndex refers to the LinkedIndex relation we made above, and TableName is an attribute of that LinkedIndex model.
Edit 2
Per your comments, it looks like you're trying to make a more complex relationship. I'll be honest that this isn't really the way you should be using linking tables (ideally you should have a linking table between two tables, not a linking table that says which 3rd table to link to) but I'll try and answer your question as best as possible within Yii.
Ideally, this relationship should be made from within the LinkedIndex model, since that's where the relationship lies.
Since you're using the table name as the linking factor, you'll need to create a way to dynamically pass in the table you want to use after the record is found.
You can use the LinkedIndex model's afterFind function to create the secondary link after the model is created within Yii, and instantiate the new linked model there.
Something like this for your LinkedIndex model:
class LinkedIndex extends CActiveRecord{
public $linked;
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return 'LinkedIndex';
}
public function afterFind(){
$this->linked = new Linked($this->TableName);
parent::afterFind();
}
//...etc.
}
The afterFind instantiates a new Linked model, and passes in the table name to use. That allows us to do something like this from within the Linked model:
class Linked extends CActiveRecord{
private $table_name;
public function __construct($table_name){
$this->table_name = $table_name;
}
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return $this->table_name;
}
//...etc.
}
which is how we dynamically create a class with interchangeable table names. Of course, this fails of the classes need to have separate operations done per-method, but you could check what the table_name is and act accordingly (that's pretty janky, but would work).
All of this would result in being to access a property of the linked table via (from within the Table model):
$this->linkedIndex->linked->foo;
Because the value of LinkedIndex.TableName and Table.linkedID is needed to get the values, I moved the afterFind, suggested by M Sost, directly into the Table-Class and changed its content accordingly. No more need for a virtual model.
class Table extends CActiveRecord {
public $linked; // Needs to be public, to be accessible
// ...etc.
public function afterFind() {
$model = new $this->linkedIndex->TableName;
$this->linked = $model::model()->findByPk( $this->linkedID );
parent::afterFind();
}
// ...
}
Now I get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;