Laravel 8 Class Based Model Factories - laravel

I'm trying to figure out why when I try and create a factory of my Player class and dd($this) it comes back as that its not overriding the state of the model. What also doesn't make sense is that it is a collection of two items for the states.
Can anyone give further clarification for any of this?
$player = Player::factory()->injured()->create();
<?php
namespace Database\Factories;
use App\Enums\PlayerStatus;
use App\Models\Player;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class PlayerFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected string $modelClass = Player::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition(): array
{
return [
'name' => $this->faker->name,
'height' => $this->faker->numberBetween(60, 95),
'weight' => $this->faker->numberBetween(180, 500),
'hometown' => $this->faker->city.', '.$this->faker->state,
'status' => PlayerStatus::__default,
];
}
public function injured(): self
{
$this->state([
'status' => PlayerStatus::INJURED,
]);
dd($this);
$now = now();
$start = $now->copy()->subDays(2);
$this->hasEmployments(1, ['started_at' => $start]);
$this->hasInjuries(1, ['started_at' => $now]);
return $this;
}
}
^ Database\Factories\PlayerFactory^ {#2650
#modelClass: "App\Models\Player"
#model: null
#count: null
#states: Illuminate\Support\Collection^ {#2647
#items: array:2 [
0 => Closure()^ {#2631
class: "Illuminate\Database\Eloquent\Factories\Factory"
this: Database\Factories\PlayerFactory {#2626 …}
use: {
$state: []
}
}
1 => Closure()^ {#2646
class: "Illuminate\Database\Eloquent\Factories\Factory"
this: Database\Factories\PlayerFactory {#2648 …}
use: {
$state: []
}
}
]
}
#has: Illuminate\Support\Collection^ {#2610
#items: []
}
#for: Illuminate\Support\Collection^ {#2640
#items: []
}
#afterMaking: Illuminate\Support\Collection^ {#2455
#items: []
}
#afterCreating: Illuminate\Support\Collection^ {#2453
#items: []
}
#connection: null
#faker: null
}

The method call:
$this->state([
'status' => PlayerStatus::INJURED,
]);
does not modify the current object - it creates a new Factory Instance with the updated 'status'. And this new Factory should be returned by the injured method.
In this case one solution would be:
public function injured(): self
{
$injuredPlayer = $this->state([
'status' => PlayerStatus::INJURED,
]);
$now = now();
$start = $now->copy()->subDays(2);
$injuredPlayer->hasEmployments(1, ['started_at' => $start]);
$injuredPlayer->hasInjuries(1, ['started_at' => $now]);
return $injuredPlayer;
}

Related

Laravel 8: Passing Factory properties into children relationships

we are currently working on a laravel 8 application. We are trying to create factories to create some dummy data for manual / developer based application testing.
The current code of my main Database-Seeder is below:
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call([
UserTableSeeder::class,
]);
\App\Models\User::factory(10)->create();
\App\Models\Activity::factory(5)->create();
/* 1. try
$tenFact = \App\Models\Tenant::factory(2)->has(
\App\Models\Project::factory(2)->state(
function (array $attributes, \App\Models\Tenant $tenant) {
return ['tenant_id' => $attributes['id']];
}
)->hasTasks(5)->hasLocation()
)->hasContracts(3)->create();
*/
/* Currently being used: */
\App\Models\Tenant::factory(10)->has(
\App\Models\Project::factory(5)->hasTasks(5)->hasLocation()
)->hasContracts(3)->create();
}
ProjectFactory.php:
class ProjectFactory extends Factory
{
protected $model = Project::class;
public function definition()
{
return [
'name' => 'Projekt: '. $this->faker->name,
'budget' => $this->faker->randomDigitNotNull*1000,
'progress' => $this->faker->randomDigitNotNull*10,
'budget_used' => $this->faker->randomDigitNotNull*50,
//'tenant_id' => Tenant::factory(),
'location_id' => Location::factory()->hasTenant(1),
];
}
}
LocationFactory.php:
class LocationFactory extends Factory
{
protected $model = Location::class;
public function definition()
{
return [
'name' => 'Standort: ' . $this->faker->company,
'street' => $this->faker->streetName,
'house_number' => $this->faker->buildingNumber,
'house_addition' => $this->faker->secondaryAddress,
'zip' => $this->faker->postcode,
'city' => $this->faker->city,
'tenant_id' => Tenant::factory(),
];
}
}
Our relationships look like this:
Tenant
|-- Project (has: tenant_id, but also has location_id)
| | -- Task (has: project_id)
|-- Locations (has: tenant_id)
|-- Contracts (has: tenant_id)
When creating datasets with the above named Tenant-Factory the following happens:
Tenant->id is being passed to Project(tenant_id)
but: Tenant->id is not being passend to Location (which depends on the tenants id but is also used for Project).
How can we pass the id of \App\Models\Tenant::factory(10) to Project::factory(5)->hasTasks(5)->hasLocation()?
Additionally we do have the problem, that even though we request 10 tenants, we will get around 60, because Location/Project create new objects when they should be using existing ones.
I gave up using the chained usage of the Tenant-Factory - I finally used some for-Loop that connected the related objects to each user by using laravels for() and state() methods:
for ($i=0; $i < 10 ; $i++) {
$tenant = \App\Models\Tenant::factory()->hasContracts(3)->create();
for ($j=0; $j < 5; $j++) {
$location = \App\Models\Location::factory(1)->for($tenant)->create();
$project = \App\Models\Project::factory(1)->state([
'location_id' => $location->first()['id'],
'tenant_id' => $tenant['id']])->hasTasks(5)->create();
}
}
class ProjectFactory extends Factory
{
$location_ids = App\Models\Location::pluck('id')->toArray();
protected $model = Project::class;
public function definition()
{
return [
'name' => 'Projekt: '. $this->faker->name,
'budget' => $this->faker->randomDigitNotNull*1000,
'progress' => $this->faker->randomDigitNotNull*10,
'budget_used' => $this->faker->randomDigitNotNull*50,
//'tenant_id' => Tenant::factory(),
'location_id'=> $faker->randomElement($location_ids),
];
}
}
class LocationFactory extends Factory
{
$tenant_ids = App\Models\Tenant::pluck('id')->toArray();
protected $model = Location::class;
public function definition()
{
return [
'name' => 'Standort: ' . $this->faker->company,
'street' => $this->faker->streetName,
'house_number' => $this->faker->buildingNumber,
'house_addition' => $this->faker->secondaryAddress,
'zip' => $this->faker->postcode,
'city' => $this->faker->city,
'tenant_id'=> $faker->randomElement($tenant_ids),
];
}
}

Issue with Laravel's Eloquent implicit JSON casting. Associated array casts to indexed array

I try to use casting in my Model like this one:
protected $casts = [
'thumbnails' => 'array', // also tried 'json' and 'object'
];
While saving, the correct data is put:
$thumbnails = ['small' => 'path1.jpeg', 'medium' => 'path2.jpeg', 'large' => 'path3.jpeg'];
$product->images()->create([
'path' => $imagePath,
'thumbnails' => $thumbnails
]);
// Saved content of 'thumbnails' field in DB row:
// {"large": "path1.jpeg", "small": "path2.jpeg", "medium": "path3.jpeg"}
Then I try get access to this field, I have:
$image = Image::find(6);
dd($image->thumbnails);
// array:3 [▼
// 0 => "path1.jpeg"
// 1 => "path2.jpeg"
// 2 => "path3.jpeg"
// ]
Instead of
// array:3 [▼
// 'small' => "path1.jpeg"
// 'medium' => "path2.jpeg"
// 'large' => "path3.jpeg"
// ]
Database field type is json.
My bad, I added some wrong getter in the Model...
/**
* #return array
* #throws \Exception
*/
public function getThumbnailsAttribute() : array
{
return resolve(ImageService::class)->getThumbnailUrls($this);
}

Laravel: How to get model instance after create that has a global scope

I struggle to get the model instance with global scopes.
Normally, if you write:
$offer = Offer::createNew();
with
public static function createNew()
{
return static::create([
'user_id' => auth()->id(),
'reviewed' => false
]);
}
You would get the model instance. But now I've added a global scope and cannot get it to work.
The model is "nearly" empty as you expect because in my case I want only to get Offers that are reviewed. However, if I add:
public static function createNew()
{
return static:::withoutGlobalScopes()->create([
'user_id' => auth()->id(),
'reviewed' => false
]);
}
I get a result from a limited model that only contains these attributes:
#attributes: array:5 [
"user_id" => 1
"reviewed" => false
"updated_at" => "2018-09-24 11:48:27"
"created_at" => "2018-09-24 11:48:27"
"id" => 2
]
But my model has obviously more attributes than that. If I add get(), I'm only getting
Illuminate\Database\Eloquent\Collection {#1635
#items: []
}
So how would you get the model with create when having a global scope?
Edit
My first workaround looks like this:
public static function createNew()
{
$model = static::create([
'user_id' => auth()->id(),
'reviewed' => false
]);
return static::withoutGlobalScopes()->find($model->id);
}
Edit 2
My Globalscope looks like this:
class FoodScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('reviewed', true)
->where('paused', false);
}
}
This behavior is not caused by the global scope.
Use ->fresh() when you want to get the other attributes:
public static function createNew()
{
return static::create([
'user_id' => auth()->id(),
'reviewed' => false
])->fresh();
}

Adding a new property dynamically to model (Laravel)

I'm using Laravel 5.4 and can't seem to add a new property, here's my code including a comment section that shows the output which does not return the new attribute I added called Url.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Exercice extends Model
{
protected $table = 'exercices';
protected $connection = 'DB_V2';
protected $appends = ['Url'];
public function getUrlAttribute() {
return 'custom';
}
/*
$exercise = App\Exercice::where("idExercice", 1)->get();
dd($exercise);
#attributes: array:15 [▼
"idExercice" => 1
"image1" => "160_A.jpg"
"image2" => "a.jpg"
"thumb1" => "v.jpg"
"thumb2" => "c.jpg"
"video" => "fliqz|077fzc4f478142cea8a73e586617f8a\r\n"
"draw1" => ""
"draw2" => ""
"drawthumb1" => ""
"drawthumb2" => ""
"licno" => 1000
"idusager" => 0
"code_exercice" => "XAMP160"
"shared" => "Y"
"rank" => 99999999
]
*/
}
The value is not going to show up in a dd() of the object itself, because the attribute doesn't actually exist in the $attributes property.
If you dd() the array or json output, you should see your value:
$exercise = App\Exercice::where("idExercice", 1)->get();
dd($exercise->toArray());

can't use boolean validation laravel 5

I'm using Laravel 5.2, and as documentation says:
boolean
The field under validation must be able to be cast as a boolean. Accepted input are true, false, 1, 0, "1", and "0".
So I've created a checkbox (styled like a switch from materialize), to return true when on, and false when off. Here goes the blade:
{!! Form::hidden('eh_capa', 0) !!}
Want to select as a graph cover?
<label>
Off
<input name="cover" type="checkbox" checked>
<span class="lever"></span>
on
</label>
Of course this code goes inside a form tag. I do the validation inside a Request class as said on this part of laravel documentation, here is my rules method:
public function rules()
{
$this['cover'] = $this['cover'] === 'on' ? 1 : 0;
$this['obra_id'] = $this->route('obra');
$this['arquivo'] = $this->hasFile('arquivo') ? $this->file('arquivo') : NULL;
dd($this);
return [
'cover' => 'required|boolean',
'obra_id' => 'exists:obras',
'path' => 'required',
'arquivo' => 'required|file|max:2048|mimes:pdf',
];
}
The dd() function returns my request like this:
StoreGraficoPostRequest {#441 ▼
#container: Application {#3 ▶}
#redirector: Redirector {#448 ▶}
#redirect: null
#redirectRoute: null
#redirectAction: null
#errorBag: "default"
#dontFlash: array:2 [▶]
#json: null
#convertedFiles: array:1 [▶]
#userResolver: Closure {#355 ▶}
#routeResolver: Closure {#354 ▶}
+attributes: ParameterBag {#443 ▶}
+request: ParameterBag {#440 ▼
#parameters: array:5 [▼
"_token" => "bZIpGW6UCcYHlCTZuIZMtmOrpCodWyfcbO1HgQid"
"path" => "hello.pdf"
"cover" => 1
"obra_id" => "29"
"arquivo" => UploadedFile {#438 ▶}
]
}
+query: ParameterBag {#442 ▶}
+server: ServerBag {#446 ▶}
+files: FileBag {#445 ▶}
+cookies: ParameterBag {#444 ▶}
+headers: HeaderBag {#447 ▶}
#content: ""
#languages: null
#charsets: null
#encodings: null
#acceptableContentTypes: null
#pathInfo: null
#requestUri: null
#baseUrl: null
#basePath: null
#method: "POST"
#format: null
#session: Store {#394 ▶}
#locale: null
#defaultLocale: "en"
}
But when I comment the dd function, the validation returns that cover must be true or false. The same happens if I change the value of cover field to true, "1" and "true" when on. I've searched all the web for anything that helps and got nothing... I'm beginning to think that this is a Laravel bug...
Well, I got a way to do it. The trick was just to add this code to my Request class:
protected function getValidatorInstance()
{
$data = $this->all();
$data['eh_capa'] = $data['eh_capa'] === 'on' ? 1 : 0;
$data['obra_id'] = $this->route('obra');
$this->getInputSource()->replace($data);
/* modify data before send to validator */
return parent::getValidatorInstance();
}
and then, my rules method ended only with the return.
You are modifying your input in the wrong place. You should override the all() function in your request class, and modify your input there.
public function rules()
{
return [
'cover' => 'required|boolean',
'obra_id' => 'exists:obras',
'path' => 'required',
'arquivo' => 'required|file|max:2048|mimes:pdf',
];
}
public function all()
{
$input = parent::all();
$input['cover'] = $input['cover'] === 'on' ? 1 : 0;
$input['obra_id'] = ...
$input['arquivo'] = ...
return $input;
}
I ran into the same problem and decided to create a small static class that parses all values that are marked as boolean in a rule.
The advantage is that it will only parse booleans that the rules dictate to be boolean. Any other input values will go unchanged, making it still possible to post a string with value 'true' if you desire.
<?php
namespace App\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ValidationHelper {
/**
* A recursive funciton to loop over any input and parse them to true booleans.
*/
private static function _getObj(&$inputs, $idPath) {
$idPart = array_shift($idPath);
if (count($idPath) > 0) {
if ($idPart == '*') {
for ($i = 0; $i < count($inputs); $i++) {
ValidationHelper::_getObj($inputs[$i], $idPath);
}
}
else {
ValidationHelper::_getObj($inputs[$idPart], $idPath);
}
}
else {
$currentValue = $inputs[$idPart];
if ($currentValue == 1 || strtolower($currentValue) == 'true') {
$inputs[$idPart] = true;
}
else if ($currentValue == 0 || strtolower($currentValue) == 'false') {
$inputs[$idPart] = false;
}
else { // any other value will be left untouched
$inputs[$idPart] = $currentValue;
}
}
}
/**
* Will validate a request against the given rules.
* Will also help fix any booleans that otherwise are parsed as 'true' strings.
* #param Request $request
* #param Array $rules
* #return void
*/
public static function validateRequest(Request $request, $rules) {
// Find any rules demanding booleans:
$booleanIDs = [];
foreach ($rules as $id => $rule) {
if (is_string($rule)) {
$rule = explode('|', $rule);
}
if (in_array('boolean', $rule)) {
$booleanIDs[] = $id;
}
}
if (isset($booleanIDs)) {
// Set all inputs to a bindable array:
$inputs = [];
foreach ($request->all() as $key => $value) {
$inputs[$key] = $value;
}
// Recursively loop through the boolean-ids
foreach ($booleanIDs as $id) {
$idPath = explode('.', $id);
ValidationHelper::_getObj($inputs, $idPath);
}
// Make sure the booleans are not just validated correctly but also stay cast when accessing them through the $request later on.
$request->replace($inputs);
}
else {
$inputs = $request->all();
}
$validator = Validator::make($inputs, $rules);
if ($validator->fails()) {
throw new \Exception('INVALID_ARGUMENTS', $validator->errors()->all());
}
}
}
Rules can be set as array or string (as normal) and it even works with nested values:
ValidationHelper::validateRequest($request, [
['foo'] => ['nullable', 'boolean'],
['bar'] => ['required', 'boolean'],
['item.*.isFoo'] => ['nullable', 'boolean'],
['item.*.isBar'] => 'required|boolean'],
]);

Resources