Laravel Factories with Intermediate Tables - laravel

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.

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 API APP Many-Many Relationship, how to return specific information in JSON?

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

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.

How to correctly instantiate the Iluminate class outside of laravel

I have Eloquent working outside of Laravel with no problems. Now Im trying to use also the Validation class by:
<?php
namespace User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Validator;
class User extends Model {
private $rules = array(
'firstName' => 'required|min:2|max:50',
'lastName' => 'required|min:2|max:50',
);
public function validate($data)
{
$v = Validator::make($data, $this->rules);
return $v->passes();
}
}
Executing that code give me an error:
Call to undefined method Illuminate\Validation\Validator::make()
That is correct since the method make is not on the class Validator but in his factory:
How can I correctly instantiate the Validation lib in order to get this working?
BTW, this is my composer.json:
{
"require": {
"slim/slim": "^2.6",
"illuminate/database": "^5.1",
"illuminate/validation": "^5.1"
},
"autoload": {
"classmap": [
"app/model"
]
}
}
To get the validation package to work outside of Laravel, you'll also need the translation package.
So first create an instance of the translator, and then use that to make a validation factory.
Working example
<?php
/*
Required composer packages:
illuminate/validation
illuminate/translation
*/
/*
Translation language files directory is the same as with Laravel
./lang/en/validation.php
*/
require_once 'vendor/autoload.php';
// You need to specify where the translation files is
$test_translation_path = __DIR__.'/lang';
$test_translation_locale = 'en';
// Set up data for the validator
$test_input_data = ['field' => 'value'];
$test_input_rules = ['field' => 'required'];
$translation_file_loader = new Illuminate\Translation\FileLoader(new Illuminate\Filesystem\Filesystem, $test_translation_path);
$translator = new Illuminate\Translation\Translator($translation_file_loader, $test_translation_locale);
$validation_factory = new Illuminate\Validation\Factory($translator);
$validator = $validation_factory->make($test_input_data, $test_input_rules);
if ($validator->fails()) {
die('Validation failed');
}
die('Validation passed!');
There are a few issues with your approach:
you're trying to create a new Validator instance by calling the Illuminate\Validation\Validator::make method, yet you point out that the make() method is present on the Illuminate\Validation\Factory which is a different class altogether, so the error you're getting is justified.
you're trying to call the make() method statically :: when in fact it's not defined as such.
you're trying to use the Validator the same as you would in a Laravel application enviroment, which won't work because you're missing the Laravel Facades and Service Providers infrastructure that Laravel uses to allow for such a simple instantiation of the Validator.
If you were to look at the registerValidationFactory() method inside the Illuminate\Validation\ValidationServiceProvider class, you'd get a sense of how the validator instance is created. So based on that, you could do the following:
namespace User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Factory as ValidatorFactory;
use Symfony\Component\Translation\Translator;
class User extends Model {
private $rules = array(
'firstName' => 'required|min:2|max:50',
'lastName' => 'required|min:2|max:50',
);
public function validate($data)
{
$factory = new ValidatorFactory(new Translator('en'));
$v = $factory->make($data, $rules);
return $v->passes();
}
}

load multiple models in array - codeigniter framework

<?php
class Home extends CI_Controller
{
public function __construct()
{
// load libraries //
$this->load->library('session');
$this->load->library('database');
$this->load->library('captcha');
// alternative
$this->load->library(array('session', 'database', 'captcha'));
// load models //
$this->load->model('menu_model', 'mmodel');
$this->load->model('user_model', 'umodel');
$this->load->model('admin_model', 'amodel');
// alternative
$this->load->model(array(?));
}
}
?>
How can i load all models in array? is it possible?
For models, you can do this:
$models = array(
'menu_model' => 'mmodel',
'user_model' => 'umodel',
'admin_model' => 'amodel',
);
foreach ($models as $file => $object_name)
{
$this->load->model($file, $object_name);
}
But as mentioned, you can create file application/core/MY_Loader.php and write your own method for loading models. I think this might work (not tested):
class MY_Loader extends CI_Loader {
function model($model, $name = '', $db_conn = FALSE)
{
if (is_array($model))
{
foreach ($model as $file => $object_name)
{
// Linear array was passed, be backwards compatible.
// CI already allows loading models as arrays, but does
// not accept the model name param, just the file name
if ( ! is_string($file))
{
$file = $object_name;
$object_name = NULL;
}
parent::model($file, $object_name);
}
return;
}
// Call the default method otherwise
parent::model($model, $name, $db_conn);
}
}
Usage with our variable from above:
$this->load->model($models);
You could also allow a separate DB connection to be passed in an array, but then you'd need to have a multidimensional array, and not the simple one we used. It's not too often you'll need to do that anyways.
I don't have any idea about the CodeIgniter 2.x but in CodeIgniter 3.x, this will also works :
$models = array(
'menu_model' => 'mmodel',
'user_model' => 'umodel',
'admin_model' => 'amodel',
);
$this->load->model($models);
Not natively, but you can easily extend Loader->model() to support that logic.
This work fine for me:
$this->load->model(array('menu_model'=>'menu','user_model'=>'user','admin_model'=>'admin'));

Resources