Laravel API APP Many-Many Relationship, how to return specific information in JSON? - laravel

I been trying to figure this out for some time now. Basically i got 2 models ' Recipe ', ' Ingredient ' and one Controller ' RecipeController ' .
I'm using Postman to test my API. When i go to my get route which uses RecipeController#getRecipe, the return value is as per the pic below:
Return for Get Route
If i want the return value of the get route to be in the FORMAT of the below pic, how do i achieve this? By this i mean i don't want to see for the recipes: the created_at column, updated_at column and for ingredients: the pivot information column, only want name and amount column information.
Return Value Format I Want
Recipe model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
protected $fillable = ['name', 'description'];
public function ingredients()
{
return $this->belongsToMany(Ingredient::class,
'ingredient_recipes')->select(array('name', 'amount'));
}
}
Ingredient Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Ingredient extends Model
{
protected $fillable = ['name', 'amount'];
}
RecipeController
<?php
namespace App\Http\Controllers;
use App\Ingredient;
use App\Recipe;
use Illuminate\Http\Request;
class RecipeController extends Controller {
public function postRecipe(Request $request)
{
$recipe = new Recipe();
$recipe->name = $request->input('name');
$recipe->description = $request->input('description');
$recipe->save();
$array_ingredients = $request->input('ingredients');
foreach ($array_ingredients as $array_ingredient) {
$ingredient = new Ingredient();
$ingredient->name = $array_ingredient['ingredient_name'];
$ingredient->amount = $array_ingredient['ingredient_amount'];
$ingredient->save();
$recipe->ingredients()->attach($ingredient->id);
}
return response()->json(['recipe' => $recipe . $ingredient], 201);
}
public function getRecipe()
{
$recipes = Recipe::all();
foreach ($recipes as $recipe) {
$recipe = $recipe->ingredients;
}
$response = [
'recipes' => $recipes
];
return response()->json($response, 200);
}
API Routes:
Route::post('/recipe', 'RecipeController#postRecipe')->name('get_recipe');
Route::get('/recipe', 'RecipeController#getRecipe')->name('post_recipe');
Thanks Guys!

I think your best solution is using Transformer. Using your current implementation what I would recommend is fetching only the needed field in your loop, i.e:
foreach ($recipes as $recipe) {
$recipe = $recipe->ingredients->only(['ingredient_name', 'ingredient_amount']);
}
While the above might work, yet there is an issue with your current implementation because there will be tons of iteration/loop polling the database, I would recommend eager loading the relation instead.
But for the sake of this question, you only need Transformer.
Install transformer using composer composer require league/fractal Then you can create a directory called Transformers under the app directory.
Then create a class called RecipesTransformer, and initialize with:
namespace App\Transformers;
use App\Recipe;
use League\Fractal\TransformerAbstract;
class RecipesTransformer extends TransformerAbstract
{
public function transform(Recipe $recipe)
{
return [
'name' => $recipe->name,
'description' => $recipe->description,
'ingredients' =>
$recipe->ingredients->get(['ingredient_name', 'ingredient_amount'])->toArray()
];
}
}
Then you can use this transformer in your controller method like this:
use App\Transformers\RecipesTransformer;
......
public function getRecipe()
{
return $this->collection(Recipe::all(), new RecipesTransformer);
//or if you need to get one
return $this->item(Recipe::first(), new RecipesTransformer);
}
You can refer to a good tutorial like this for more inspiration, or simply go to Fractal's page for details.
Update
In order to get Fractal collection working since the example I gave would work if you have Dingo API in your project, you can manually create it this way:
public function getRecipe()
{
$fractal = app()->make('League\Fractal\Manager');
$resource = new \League\Fractal\Resource\Collection(Recipe::all(), new RecipesTransformer);
return response()->json(
$fractal->createData($resource)->toArray());
}
In case you want to make an Item instead of collection, then you can have new \League\Fractal\Resource\Item instead. I would recommend you either have Dingo API installed or you can follow this simple tutorial in order to have in more handled neatly without unnecessary repeatition

Related

Laravel Spatie - Custom Attribute value when audit log via modal

on my modal, I have two functions which I have used to log the data when it has been changed. those are below.
namespace App\Models;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Contracts\Activity;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Receivinglogentry extends Model
{
use HasFactory;
use LogsActivity;
protected $fillable = [
'status',
'amt_shipment',
'container',
'po',
'etd_date',
'eta_date',
];
protected $casts = [
'po_ref' => 'json',
];
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()->logOnly(['*'])->logOnlyDirty();
}
public function tapActivity(Activity $activity,string $eventName)
{
$current_user = Auth::user()->name;
$event = $activity->attributes['event'];
$data = $activity->relations['subject']->attributes['container'];
$masterID = $activity->relations['subject']->attributes['id'];
$activity->description = "{$current_user} has {$event} Container : {$data}";
$activity->causer_name = $current_user;
$activity->master_id = $masterID ;
$activity->log_name = 'Receivinglogentry';
}
}
fillable data status has been stored as an integer value. but I have to log it as a string value something like PENDING or ACTIVE. any recommendation to log attributes customizably is appricated.
You can try ENUM concept but in Laravel 9 onwards. Here is a reference link - https://enversanli.medium.com/how-to-use-enums-with-laravel-9-d18f1ee35b56
There they describe about an enum UserRoleEnum:string which you can format as your own requirement.
In your case, the key itself is a number. So I suggest to make it as string as below.
enum StatusEnum:string
{
case STATUS_101 = 'Pending';
case STATUS_34 = 'Completed';
case STATUS_22 = 'On hold';
}
And then call it with your fillable status something like "STATUS_" + $fillable.status
If not using Laravel 9, you may try as below described in :
https://stackoverflow.com/a/55298089/2086966
class MyClass {
const DEFAULT = 'default';
const SOCIAL = 'social';
const WHATEVER = 'whatever';
public static $types = [self::DEFAULT, self::SOCIAL, self::WHATEVER];
and then write the rule as:
'type' => Rule::in(MyClass::$types)

Laravel Factories with Intermediate Tables

I am trying to create a factory for a BlogPost model.
A blog post belongsToMany Tag and vice versa.
There is an intermediate table (blog_post_tag) to store the relations of blogposts to tags.
I would like to seed a blog post with a number of tag names.
How can one seed a DB using factories and intermediate tables?
May have an answer here
So I can add the following below to my BlogPost seeder. This would also create tags, but I would like to get existing tags (preferably 3-5 and only if any exist).
hasAttached() accepts a factory as the first argument so this will not work.
BlogPost::factory()
->hasAttached(
Tag::factory()->count(3)
)
->create();
This is how I fill my relations with existing data:
<?php
namespace Database\Factories;
use App\Models\Alpha;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\Beta;
use App\Models\Gamma;
class AlphaFactory extends Factory {
protected $model = Alpha::class;
public function definition() {
$beta_ids = Beta::all()->pluck('id');
$gamma_ids = Gamma::all()->pluck('id');
return [
'name' => Str::slug($this->faker->unique()->realText(50)),
'number' => $this->faker->randomNumber(3),
'beta_id' => $this->faker->randomElement($beta_ids),
'gamma_id' => $this->faker->randomElement($gamma_ids)
];
}
}
If you want to be fancy and create sometimes new entries to relate to:
<?php
namespace Database\Factories;
use App\Models\Alpha;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\Beta;
class AlphaFactory extends Factory {
protected $model = Alpha::class;
public function definition() {
$beta_id;
$beta_ids = Beta::all()->pluck('id');
if($beta_ids->isEmpty() || $this->faker->boolean($chanceOfGettingTrue = 50)) {
$beta_id = Beta::factory()->create()->id;
} else {
$beta_id = $this->faker->randomElement($beta_ids);
}
return [
'name' => Str::slug($this->faker->unique()->realText(50)),
'number' => $this->faker->randomNumber(3),
'beta_id' => $beta_id
];
}
}
I don't know if this is the right way to do this, but it works for me. If there is a better way, let me know.

yii2 insert concatenated string into database with activerecord rules

I have a form, where users pick some options, and I would like to insert into DB an additional, concatenated field, based on user inputs. I need to do it on SCENARIO_CREATE (and update). E.g.:
type: A
size: 100
color: black
concatenated: A100black
I've tried it this way:
Model:
class Xyz extends BaseXyz {
const SCENARIO_CREATE = 'create';
public function rules() {
...
['concatenated', 'generateConcatenated', 'on' => self::SCENARIO_CREATE],
...
public function generateConcatenated() {
return $this->type . $this->size . $this->color;
}
Controller:
class XyzController extends base\XyzController {
public function actionCreate() {
$model = new Xyz;
$model->scenario = Xyz::SCENARIO_CREATE;
...
I've tried with 'filter' also in rules, but no success. Maybe my approach is totally wrong, and it can't be done in rules? Please point me in the right direction. Many thanks!
rules are for checking attribute validation but you can do it in rule too:
public function generateConcatenated($attribute,$param) {
$this->concatenated = $this->type . $this->size . $this->color;
}
I think best logistic way to achieve this is to remove attribute from rules and overriding beforeSave() in your model:
public function beforeSave($insert)
{
$this->concatenated = $this->type . $this->size . $this->color;
return parent::beforeSave($insert);
}
you should consider that yii have default insert scenario for new record and update for existing record.

Calling same eloquent statement in several controllers

I have an eloquent statement like this:
$constraint = function ($query) {
$query->where('session', Session::getId());
};
$selectedImages = ImageSession::with(['folder' => $constraint])
->whereHas('folder', $constraint)
->where('type', 'single')
->get();
Which I need to call in several controllers.
How is the best way to do it without putting this code every time?
Should I put this code in the Model? but how I put the ImageSession::with if it is inside the same model that has ImageSession class?
In the controller do I have to write...
$imageSession_table = new ImageSession;
$selectedImages = $imageSession_table->getSelectedImages();
Well there are several solutions to this, but one rule that I have learned is whenever you are doing copy paste in the same file it means you need to create a function to encapsulate that code.
The same applies when you are copying and pasting the same code over classes/controllers it means you need to create a class that will have a method, that will encapsulate that code.
Now you could in fact change your model and this depends on your application and what kind of level of abstraction you have.
Some people tend to leave the models as pure as possible and then use transformers, repositories, classes whatever you want to call it. So the flow of communication is something like this:
Models -> (transformers, repositories, classes) -> Controllers or other classes
If that's the case just create a ImageSessionRepository and in there have your method to get the selected images:
<?php namespace Your\Namespace;
use ImageSession;
use Session;
class ImageSessionRepository
{
protected $imageSession;
public function __construct(ImageSession $imageSession)
{
$this->imageSession = $imageSession;
}
public function getSelectedImages($sessionId = false){
if(!$sessionId){
$sessionId = Session::getId()
}
$constraint = function ($query) use ($sessionId){
$query->where('session', $sessionId);
};
$selectedImages = ImageSession::with(['folder' => $constraint])
->whereHas('folder', $constraint)
->where('type', 'single')
->get();
return $selectedImages;
}
}
Then on your controller you just inject it:
<?php namespace APP\Http\Controllers;
use Your\Namespace\ImageSessionRepository;
class YourController extends Controller
{
/**
* #var ImageSessionRepository
*/
protected $imageSessionRepository;
public function __construct(ImageSessionRepository $imageSessionRepository)
{
$this->imageSessionRepository = $imageSessionRepository;
}
public function getImages()
{
$selectedImages = $this->imageSessionRepository->getSelectedImages();
//or if you want to pass a Session id
$selectedImages = $this->imageSessionRepository->getSelectedImages($sessionID = 1234);
//return the selected images as json
return response()->json($selectedImages);
}
}
Another option is adding that code directly into your Model, using scopes, more info here
So on your ImageSession Model just add this function:
public function scopeSessionFolder($query, $session)
{
$constraint = function ($constraintQuery) use ($sessionId){
$query->where('session', $sessionId);
};
return $query->with(['folder' => $constraint])
->whereHas('folder', $constraint);
}
And on your controller just do this:
$selectedImages = ImageSession::sessionFolder(Session::getId())
->where('type', 'single')
->get();
Or you can include everything in your scope if that's your case
public function scopeSessionFolder($query, $session)
{
$constraint = function ($constraintQuery) use ($sessionId){
$query->where('session', $sessionId);
};
return $query->with(['folder' => $constraint])
->whereHas('folder', $constraint);
->where('type', 'single');
}
And then again on your controller you will have something like this:
$selectedImages = ImageSession::sessionFolder(Session::getId())
->get();
Just a side note I haven't tested this code, so if you just copy and paste it it's possible that you find some errors.

Laravel 4: Why can't I use aliases in my laravel code

Here's my code:
public function getReports()
{
$reports=DB::table('reports')
->select(array('*',DB::raw('reports.id','reports.departmentID','reports.officerID','reports.created_at','reports.report','reports.title','department.name','posts.name AS pname')))
->leftjoin('department','reports.departmentID','=','department.id')
->leftjoin('posts','reports.postID','=','posts.id')
->leftjoin('users','reports.officerID','=','users.id')
->leftjoin('events','events.id','=','reports.eventID')
->orderBy('reports.id','DESC')
->get();
$reportsArray=array();
foreach($reports as $row)
{
$reportsArray[]=array(
'id'=>$row->id,
'report'=>$row->report,
'title'=>$row->title,
'departmentName'=>$row->name,
'created_at'=>$row->created_at,
'post'=>$row->pname,
);
}
return View::make('reports.reports')
->with('reports',$reportsArray);
}
I'm getting the following error when I load the view: Undefined property: stdClass::$pname
This is basically a problem because i need to use aliases in many instances. And the error is basically the same when I try to use a column alias with any column. Can anyone see where the problem is with my code?
My Model:
class Report extends Eloquent
{
protected $table = 'reports';
protected $fillable= array(
'title','reportDate',,'departmentID','date','eventID','officerID','postID‌​',
);
}

Resources