In a Laravel project I have to store some data in json. For phpUnit tests I use a factory with faker. I try to fake a json structure for the tests, but it always fail on validation. Is there any proper way to create a json in factory that passes the validation for json?
I tried a simple json array, and the json array with json_encode, both of them failed at validation, and gives errors.
With simple json like: 'settings' => ['areas' => ['full', 'city']]
the error is:
Property [settings] is not of expected type [json].
Failed asserting that an array contains 'array'.
With json_encode like: 'settings' => json_encode(['areas' => ['full', 'city']])
the error is:
Property [settings] is not of expected type [json].
Failed asserting that an array contains 'string'.
My model:
class Example extends Model
{
protected $fillable = [
'name',
'settings'
];
public static $rules = [
'name' => 'required|string|max:255',
'settings' => 'nullable|json'
];
protected $casts =
'settings' => 'array'
];
}
My factory:
<?php
class ExampleFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Example::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->words(3, 7),
'settings' => json_encode(['areas' => ['full', 'city']]) // or what?
];
}
}
In my test file:
/** #test */
public function shouldStore(): void
{
$item = $this->model::factory()->make();
$data = $item->toArray();
$this->post(action([$this->controller, 'store']), $data)
->assertOk();
}
Your issue is that you are casting the property to array, but you are storing a string, you should be passing an array.
Do this:
class ExampleFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Example::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->words(3, 7),
'settings' => ['areas' => ['full', 'city']],
];
}
}
And the rule (no idea where are you using that) should be like this:
'settings' => 'nullable|array'
Your model works just fine with an array because the cast takes care of the conversion for you. However when posting to the controller you need to manually cast the data to JSON:
public function shouldStore(): void
{
$item = $this->model::factory()->make();
$data = $item->toArray();
$data['settings'] = json_encode($data['settings']);
$this->post(action([$this->controller, 'store']), $data)
->assertOk();
}
However this does mean that you may need to json_decode the data in the controller before creating the model.
Alternatevely you can do what #matiaslauriti is suggesting and post the data as an array to begin with
You need to cast the property to array:
SomeModel extends Model
{
protected $casts = [
'settings' => 'array',
];
}
Related
I'm using Laravel v6 and writing Resource and resource collection. I would like to use some data from the resource inside the resource collection. For example, I have the following UserResource and UserCollection:
class UserResource extends JsonResource
{
public $data = 0;
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$this->data + = 5;
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
public function getAdditionalData(){
return $this->data;
}
}
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection,
'additional-data' => [
(new UserResource(null))->getAdditionalData(),
],
];
}
}
I want to return some data calculated in the resource and use it in the resource collection. The value that i'm receiving is 0 while I'm expecting 5. How can I return this data from the resource into the collection?
You have not called the resource's toArray() function, so your data is not being incremented. Try calling the function first, for example:
$userResource = new UserResource(null);
$userResource->toArray(); // Returns an array. Your data is incremented here
$additionalData = $userResource->getAdditionalData(); // what you actually want
It would help if you provided a bigger picture of what you are trying to do.
**Use this method in Collection below toArray Method**
public function toArray($request) {
return $this->collection;
}
public function withResponse($request, $response) {
$jsonResponse = json_decode($response->getContent());
$jsonResponse->settings = array_values(ResponseManager::getResult('', $this->status, $this->message, $this->code))[0];
if (isset($jsonResponse->links) && isset($jsonResponse->meta)) {
unset($jsonResponse->links);
}
$response->setContent(json_encode($jsonResponse));
}
I am working on a Laravel 8 app with users and posts.
The objective is to create a bunch of posts (I already have users).
namespace Database\Factories;
// import Post model
use App\Models\Post;
// import User model
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory {
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Post::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition() {
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => $this->faker->factory(App\Models\User::class),
];
}
}
The problem
I run php artisan tinker then Post::factory()->count(100)->create() in the terminal and I get:
InvalidArgumentException with message 'Unknown format "factory"'
UPDATE
I replace my return statement with:
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
I get this in the terminal:
Class 'Database\Factories\UserFactory' not found
Questions:
Where is my mistake?
Does the fact that I get the error Class 'Database\Factories\UserFactory' not found mean that I need to
create a UserFactory factory? Because there isn't one. (I wanted
to create posts, not users).
I don't suppose there is $this->faker->factory(..).
You can do
'user_id' => App\Models\User::factory()->create()->id,
EDIT:
'user_id' => App\Models\User::factory(),
Creating a UserFactory factory and using the below return statement did the trick:
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
So, the PostFactory class looks like this:
class PostFactory extends Factory {
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Post::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition() {
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
}
}
I'm trying to run a faker factory for relationships, but the field always returns NULL. How do fake a model relationship without hitting the database?
I have a Map factory with a one-to-one relationship to a parent Event table. I need to fake this relationship for unit testing:
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'event' => 'faker.' . join('_', $this->faker->words),
'category' => $this->faker->word,
'sub_category' => $this->faker->word,
'priority' => $this->faker->randomElement(['normal', 'high']),
'event' => Event::factory()->makeOne(),
];
}
This returns a fake model, but event is null, from the debugger:
result = {array} [5]
event = "faker.eum_voluptatibus_aut"
category = "libero"
sub_category = "aut"
priority = "high"
event = null
I tried using states, but the same thing happens:
public function disabled()
{
return $this->state([
'event' => Event::factory()->makeOne(['enabled' => false]),
]);
}
The object is returned with an empty event value. I need a faker object I can transverse down into: if ($object->event->enabled) [...]. How do I generate fake model relationships?
If you are using Laravel 8.x you must consider using methods used on the docs, it must look like that :
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'event' => 'faker.' . join('_', $this->faker->words),
'category' => $this->faker->word,
'sub_category' => $this->faker->word,
'priority' => $this->faker->randomElement(['normal', 'high']),
'event_id' => Event::factory(),
];
}
/**
* Indicate that the map is disabled.
*
* #return \Illuminate\Database\Eloquent\Factories\Factory
*/
public function disabled()
{
return $this->state([
'event_id' => Event::factory()->create(['enabled' => false]),
]);
}
The only solution I found, so far, is to manually set the event key myself in my tests. It's not the ideal or elegant solution.
$fieldMap = Map::factory()->makeOne();
$fieldMap->event = Event::factory(['enabled' => false])->makeOne();
I don't like this approach. Why can't I define factories within factories?
I want to store an array as a string in a open_hours field. How i can do it.
Please help me.thats my code . Thanks in advance
$company = Company::create([
'company_name' => $request->input('company_name'),
'company_picture'=> $company_picture,
'address' => $request->input('address'),
'latitude' => $request->input('latitude'),
'longitude' => $request->input('longitude'),
'zipcode' => $request->input('zipcode'),
'city' => $request->input('city'),
'country' => $request->input('country'),
'open_hours' => $request->input('open_hours'),
'subcategory_id' => $request->input('subcategory_id'),
'price' => $request->input('price'),
'age_limit' => $request->input('age_limit'),
'company_description' => $request->input('company_description'),
]);
You could store it a JSON in the database and make Laravel to handle it (cast) as an array.
From the documentation:
Array & JSON Casting
The array cast type is particularly useful when working with columns
that are stored as serialized JSON. For example, if your database has
a JSON or TEXT field type that contains serialized JSON, adding
the array cast to that attribute will automatically deserialize the
attribute to a PHP array when you access it on your Eloquent model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be cast to native types.
*
* #var array
*/
protected $casts = [
'options' => 'array',
];
}
Once the cast is defined, you may access the options attribute and
it will automatically be deserialized from JSON into a PHP array. When
you set the value of the options attribute, the given array will
automatically be serialized back into JSON for storage:
$user = App\User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
So, you can do this in your Company model:
app/Company.php
class Company extends Model
{
/**
* The attributes that should be cast to native types.
*
* #var array
*/
protected $casts = [
'open_hours' => 'array',
];
}
Of course, you should have the open_hours column defines as a JSON/TEXT column:
database/migrations/create_companies_table.php // your migration file
public function up()
{
Schema::create('companies', function (Blueprint $table) {
// ...
$table->json('open_hours');
// ...
});
}
Update
Now, whenever you get a Company instance, you should be able to handle your desired field as an array:
ACoolController.php
// get your instance
$company = App\Company::find(1);
// return it to a view
return view('path.to.view')->with('company', $company);
Then in your view:
path/to/view.blade.php
#foreach ($company->open_hours as $open_hour)
<p> This is an {{ $open_hour }} </p>
#endforeach
Use open_hours column as json to store array values. Laravel migration has column type ->json('column_name')
I've been working on creating a clean interface for our various web application and I've run into a snag with Laravel's API Resources not properly converting the incoming json array into laravel collections.
I can do it with a single resource:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
use App\Product;
class ProductResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'name' => $this->resource['product']['name'],
'description' => $this->resource['product']['description'],
'price' => $this->resource['product']['rental_rate']['price']
];
//return parent::toArray($request);
}
}
print this response outputs:
{"name":"Arri Skypanel S60-C","description":"Arri Sky Panel S60-C 450w input with a 2000w tungsten equivalent & Combo Stand","price":"260.0"}
However trying to take this single item and turn it into a collection of items isn't going anywhere.
Anybody got a clue what I'm missing?
Pulling the API data looks like this:
namespace App;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Client;
class ThirPartyAPI
{
private $url = 'https://api.third-party.com/api/v1/';
public function pull($query, $additionalParams) {
$client = new Client;
$result = $client->get($this->url . $query . $additionalParams, [
'headers' => [
'Content-Type' => 'application/json',
'X-AUTH-TOKEN' => env('CURRENT-AUTH-TOKEN'),
'X-SUBDOMAIN' => env('CURRENT-SUBDOMAIN')
]
]);
$array = json_decode($result->getBody()->getContents(), true);
return $array;
}
}
The API returns a lot of json data.
This is the Product model:
public function getAllProducts() {
try {
$productData = [];
$query = "/products?page=1&per_page=3&filtermode=active";
$additionalParams = "";
$productData = new ThirdPartyAPI;
$productData = $productData->pull($query, $additionalParams);
$productData = $productData['products'];
return ProductsResource::make($productData);
} catch (\Exception $ex) {
return $ex;
} catch (\Throwable $ex) {
return $ex;
}
}
Right now I'm trying something this to convert all the returned arrays into something I can control more:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\Resource;
class ProductsResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'products' => $this->collection->mapInto(function($request) {
return[ 'name' => $this->resource['name'],
'description' => $this->resource['description'],
'price' => $this->resource['rental_rate']['price']
];
})
];
}
However var_dumping the data just returns this:
object(App\Http\Resources\ProductsResource)[200]
public 'resource' =>
array (size=3)
0 =>
array (size=37)
'id' => int 164
'name' => string '10A Dimmer' (length=10)
[Lots of data]
...
'sale_rates' =>
array (size=0)
...
1 => .....
[cont]
public 'with' =>
array (size=0)
empty
public 'additional' =>
array (size=0)
empty
I've tried various forms of data conversion on the return json info and haven't had a lot of results except errors and confusing business. I'm a little shady on how Laravel handles API Resource handling.
Ok after some investigation into Laravel's 'make', 'mapinto' and 'map' methods for collections, I eventually got a working result from this conversion here:
$productData = ThirdPartyAPI;
$productData = $productData->pull($query, $additionalParams);
$productData = $productData['products'];
$products = collect($productData)->map(function($row){
return ProductsResource::make($row)->resolve();
});
var_dump($products);
That var_dump returns this:
object(Illuminate\Support\Collection)[228]
protected 'items' =>
array (size=3)
0 =>
array (size=3)
'name' => string '10A Dimmer' (length=10)
'description' => string '10amp Dimmer (Max 2.4k)' (length=23)
'price' => string '5.0' (length=3)
....
[And so on]
The initial information that was returned was a multidimensional array.
$returnedArray = array(
['array' => 1, 'name' => 'name', etc],
['array' => 2, 'name' => 'name, etc]
);
Laravel's default collection method only turns the top array into a collection. In order to properly be able to control the results via the Resource models we have to convert the whole set of arrays to collections, which means we have to iterate through the returned data to convert it to something laravel can read properly. That's what the map method does.
According to the docs it, 'The map method iterates through the collection and passes each value to the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items'
the make method creates a new collection instance. I don't know what the resolve function does except that the docs mention that it 'resolves a given class or interface name to its instance using the service container'. I'm going to assume it means that it makes sure passes through the class properly?
Anyway I hope that helps people in the future.