How to change default Yii 2 Rest API GET response result - php

According to Yii 2 Rest APi documentation, I have a CountriesCountry that extends \yii\rest\ActiveController and a corresponding Countries model. This is the code for my Controller class.
<?php
namespace app\controllers;
class CountriesController extends \yii\rest\ActiveController{
public $modelClass = 'app\models\Countries';
public function actionIndex(){
}
public function actionView(){
}
public function actionCreate(){
}
public function actionUpdate(){
}
public function actionDelete(){
}
public function actionOptions(){
}
}
When I send a get request, it returns all the countries in my database.
My Question
is it possible to return my own result from action methods. Like in the actionIndex(), I will like to limit the result to 20 records. I did something like this but it is not working.
public function actionIndex(){
$model = Countries::find()->limit(20);
print_r($model);
}
I know that I can get all the countries from by database and loop through it and obtain only 20 results but I want to just query for 20 records from database.

Your class CountriesController that extends from \yii\rest\ActiveController automatically supports GET, PUT, POST calls etc. No need for actionIndex(), actionCreate(), etc if you just want regular REST functionality. Read about it in the Yii2 guide.
To limit the results you could just set another page size in your controllers afterAction-method. Add this to your controller. (I believe that 20 records is default of Pagination class, so if that is what you want you don't need this code at all. Just use the default functionality of yii/rest/ActiveController.)
public function afterAction($action, $result) {
if (isset($result->pagination) && ($result->pagination !== false)) {
$result->pagination->setPageSize(100);
}
return parent::afterAction($action, $result);
}

Suppose your api link is:
http://localhost/yii2-rest/api/country/?limit=15&order=id
Controller:
public function actionIndex(){
$model = Countries::find()
->orderBy($_GET['order'])
->limit($_GET['limit'])
->all();
return $model;
}
Take care about security!
You can get query string this way:
$limit = Yii::app()->getRequest()->getQuery('limit');

Straightway
//SELECT * FROM countries LIMIT 20
$countries= Countries::find()->limit(20)->all();

Related

Yii2 model relations not returned in json

Am fetching data with javascript to a yii2 api which i would like to also return model relations.
I have the following
In my user class i have
class User{
//relationship
public function getAuthOptions(){
return $this->hasMany(UserAuthOption::className(),["user_id"=>"id"]);
}
}
Am fetching the data as follows
$users = User::find()->with(['authOptions'])->all();
return $users.
The above returns an array of objects which doesnt contain the authOptions.
I understand that you can access the relationship data via
$users[0]->authOptions
But is there a way the relationship data can be returned on the $users query for javascript api requests which cannot access the $users[0]->authOptions
Currently am able to achieve this by adding a custom field like
class User{
public function fields()
{
$fields = parent::fields();
$fields["authOptions"]=function ($model){
return $model->authOptions;
};
return $fields;
}
public function getAuthOptions(){
return $this->hasMany(UserAuthOption::className(),["user_id"=>"id"]);
}
}
But the above is not optimal because it returns authOptions in all requests but i would like to controll which requests require authOptions to be returned.
class User extends ActiveRecord
{
public function extraFields()
{
return [
'authOptions',
];
}
public function getAuthOptions() {
return $this->hasMany(UserAuthOption::class, ['user_id' => 'id']);
}
}
After that you can use expand param when you need in your API query like this:
/api/controller/action?expand=authOptions
->with(['authOptions']) is not necessary in REST Controller.

Get first image for each product

I try to get latest 10 products with its images but only the first image
so that is what i try
$newProducts = \App\Product::latest()->with(['images', function($el){
$el->first();
}])->with('category')->take(10)->get();
but it gives me this error
mb_strpos() expects parameter 1 to be string, object given
it has a morph relation between product and image
Product Model
class Product extends Model {
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}
Image Model
class Image extends Model {
public function imageable()
{
return $this->morphTo();
}
}
The above solutions are all good. I personally prefer a different solution that I think is gonna be ideal.
I am gonna define a different relationship for a product:
class Product extends Model {
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
public function firstImage()
{
return $this->morphOne(Image::class, 'imageable');
}
}
So you can access the first image directly or eager load the relationship:
$product->firstImage;
$product->load('firstImage');
Product::with('firstImage');
Just FYI, I learnt about this and other useful database tricks from Jonathan Reinink in Laracon Online 2018.
When using with as a key value array, the $el parameter to the closure will be a query builder that has not executed yet.
The way to limit query builders of results is to use take(). Therefor your code should look like this.
->with(['images', function($el) {
$el->take(1);
}])
Edit To make this solution work, you will need an extra package. Using the following trait should make it work and using limit instead. See the following post.
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
->with(['images', function($el) {
$el->limit(1);
}])
Alternatively Laravel solution is to use transformation like properties, where you can create your own custom properties, in the function naming starting with get and ending with attribute.
class Product {
protected $appends = ['first_image'];
public function getFirstImageAttribute() {
return $this->images->first();
}
}
Now if you use standard Laravel serialization all products will have an first_image field and in your code you can access it like so.
$product->first_image;
To avoid performance hits, include images using with('images').
public function images()
{
return $this->hasMany(Image::class);
}
public function firstImage()
{
return $this->images()->first();
}
Simply create a function that defines relationship between product and its images.
Then create a function that gets the first image

How to randomize results of ResourceCollection but still paginate api results

I have setup an API in Laravel that returns paginated results of Albums that are stored in my database. It currently returns 15 results with the rest paginated, however I want to randomize the results before paginating.
This is the code in my api.php
<?php
use Illuminate\Http\Request;
use App\Album;
Route::apiResource('/album', 'AlbumController');
This is the code for AlbumController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Album;
use App\Http\Resources\AlbumResource;
use App\Http\Resources\AlbumResourceCollection;
class AlbumController extends Controller
{
/**
* Return AlbumResource which wraps all data in an object named data.
*
* #param Album $album
* #return AlbumResource
*/
public function show(Album $album): AlbumResource {
return new AlbumResource($album);
}
public function index(): AlbumResourceCollection {
return new AlbumResourceCollection(Album::paginate());
}
}
I tried altering the index() function and I was able to randomize 15 results, however, I end up losing pagination. I did accomplished that with changing the function to this:
public function index(): AlbumResourceCollection {
return new AlbumResourceCollection(Album::all()->random(15));
}
What I want is something along the lines of this:
Get all results
Randomize results
Paginate it
I also attempted this but I got an error:
public function index(): AlbumResourceCollection {
return new AlbumResourceCollection(Album::all()->random()->paginate());
}
Any thoughts on what I can do to accomplish this?
The solution to my problem was as lagbox mentioned in the comments.
I updated my index function to the following:
public function index(): AlbumResourceCollection {
return new AlbumResourceCollection(Album::inRandomOrder(rand(0, 5000))->paginate());
}

Saving object in a class variable and using it in another function - php, laravel

So here's the code
use App\Video;
class HomeController extends Controller
{
protected $video;
public function index()
{
// $video_to_watch is fetched from db and I want to save it and use it in
// another function in this controller
$this -> video = $video_to_watch;
return view('home', compact('video_to_watch'));
}
public function feedback(Request $request)
{
dd($this -> video);
}
}
feedback returns null for some reason.
when I put the
dd($this -> video);
in index() it works fine, not null.
I have tried what's suggested here: Laravel doesn't remember class variables
but it didn't help.
I'm sure it's something stupid I'm overlooking. But can't seem to figure out what, any help much appreciated.
You can't keep your $video value between 2 different requests. You have to fetch your video data in each request.
use App\Video;
class HomeController extends Controller
{
public function index() {
$myVideo = $this->getMyVideo();
return view('home', $myVideo);
}
public function feedback(Request $request) {
dd($this->getMyVideo);
}
private function getMyVideo() {
// fetch $video_to_watch from db
return $video_to_watch ;
}
}
First of all don't fetch data inside a Controller. It's only 'a glue' between model and view. Repeat. No fetching inside a controller.
Use domain services and dependency injection to get business data and if you want to share this data create shared service (single instance).
-
Putting a data object into a controller property class makes a temporary dependency between method calls. Avoid it. Use services instead.

Laravel Eloquent setting a default value for a model relation?

I have two models:
class Product extends Eloquent {
...
public function defaultPhoto()
{
return $this->belongsTo('Photo');
}
public function photos()
{
return $this->hasMany('Photo');
}
}
class Photo extends Eloquent {
...
public function getThumbAttribute() {
return 'products/' . $this->uri . '/thumb.jpg';
}
public function getFullAttribute() {
return 'products/' . $this->uri . '/full.jpg';
}
...
}
This works fine, I can call $product->defaultPhoto->thumb and $product->defaultPhoto->full and get the path to the related image, and get all photos using $product->photos and looping through the values.
The problem arises when the product does not have a photo, I can't seem to figure out a way to set a default value for such a scenario.
I have tried doing things such as
public function photos()
{
$photos = $this->hasMany('Photo');
if ($photos->count() === 0) {
$p = new Photo;
$p->url = 'default';
$photos->add($p);
}
return $photos;
}
I have also creating a completely new Collection to store the new Photo model in, but they both return the same error:
Call to undefined method Illuminate\Database\Eloquent\Collection::getResults()
Has anyone done anything similar to this?
Thanks in advance!
You could create an accessor on the Product model that did the check for you. Works the same if you just wanted to define it as a method, also (good for if you want to abstract some of the Eloquent calls, use an interface for your Product in case you change it later, etc.)
/**
* Create a custom thumbnail "column" accessor to retrieve this product's
* photo, or a default if it does not have one.
*
* #return string
*/
public function getThumbnailAttribute()
{
$default = $this->defaultPhoto;
return ( ! is_null($default))
? $default->thumb
: '/products/default/thumb.jpg';
}
You might also want to look into Presenters. A bit overkill for some situations, but incredibly handy to have (and abstract things like this away from your models).

Categories