A different way to version api output with laravel? - laravel

I'm about to ...
extend my App/Orm/MyModel.php with Http/Json/V1/MyModel.php so I can keep the $appends, $hides, toArray() neatly tucked away in a V1
namespace and prefix some routing for V1
probably do some custom resolvers for route model binding
And I'm thinking ... really? They haven't built this in... what am I missing here? There's gotta be a quick, turn-key for this. I'm interested in knowing how other people are doing this, so please chime in.

Try Resources instead of Models
Have a look at resources:
https://laravel.com/docs/5.7/eloquent-resources
And add your logic to resources so that you display different versions of a model depending on the API version. You can still make use of $appends and $hidden.
With this approach we return a Resource of a model rather than the model itself.
Here is an example of a UserResource for different API versions:
class UserResource extends JsonResource
{
private $apiVersion;
public function __construct($resource, int $apiVersion = 2) {
$this->apiVersion = $apiVersion; // OPTION 1: Pass API version in the constructor
parent::__construct($resource);
}
public function toArray($request): array
{
// OPTION 2: Get API version in the request (ideally header)
// $apiVersion = $request->header('x-api-version', 2);
/** #var User $user */
$user = $this->resource;
return [
'type' => 'user',
'id' => $user->id,
$this->mergeWhen($this->apiVersion < 2, [
'name' => "{$user->first_name} {$user->last_name}",
], [
'name' => [
'first' => $user->first_name,
'last' => $user->last_name
],
]),
'score' => $user->score,
];
}
}
The you can call:
$user = User::find(5);
return new UserResource($user);
If you need a different connection you can do:
$user = User::on('second_db_connection')->find(5);
So V1 API gets:
{
id: 5,
name: "John Smith",
score: 5
}
and V2 API gets:
{
id: 5,
name: {
first: "John",
last: "Smith",
},
score: 5
}
Now if later you wanted to rename score to points in your DB, and in V3 of your API you also wanted to change your JSON output, but maintain backwards compatibility you can do:
$this->mergeWhen($this->apiVersion < 3, [
'score' => $user->points,
], [
'points' => $user->points,
])
Prefix routes
You can easily prefix routes as mentioned here: https://laravel.com/docs/5.7/routing#route-group-prefixes
Route::prefix('v1')->group(function () {
Route::get('users', function () {
// ...
});
});
Explicit Route Model Binding
To do custom route model bindings have a look at: https://laravel.com/docs/5.7/routing#route-model-binding
e.g.
Route::bind('user', function ($value) {
return App\User::where('name', $value)->first() ?? abort(404); // your customer logic
});

Related

Merge FormRequest rules

This question is based on this thread: Merge 2 rules FormFequest for validate an update and store action in laravel5.5
Context: Let's suppose I have these 2 requests and I want to merge the SocialMediaFormRequest rules in ReadersFormRequest rules.
ReadersFormRequest
class ReadersFormRequest extends FormRequest
{
public function rules(SocialMediaFormRequest $social)
{
$mediaRules = $social->rules();
$rules = [
'first_name'=>'required',
'last_name'=>'required',
'birthday'=>'required',
'region'=>'required',
'photo_url'=>'required',
'support'=>'required',
'riwayas_id'=>'required',
'description'=>'required',
];
return array_merge($rules,$mediaRules);
}
}
SocialMediaFormRequest
class SocialMediaFormRequest extends FormRequest
{
public function rules()
{
return [
'url'=>'required|url',
'title'=>'required'
];
}
}
Form that I received
first_name: "example"
last_name: "example"
birthday: 2022-06-13
region: somewhere
photo_url: "https:XXX"
support: false
riwayas_id: 1
description: ""
media.url: "https:YYY"
media.title: "stackoverflow"
Question: How can I only pass the argument media.XXX in my form SocialMediaFormRequest?
You can use prepareForValidation() method in the form request to sanitize the inputs : https://laravel.com/docs/9.x/validation#preparing-input-for-validation
So, if in SocialMediaFormRequest you receive the full request you can only get the required fields like that:
public function prepareForValidation()
{
$this->replace([
'url' => $this->url ?? ($this->media['url'] ?? null),
'title' => $this->title ?? ($this->media['title'] ?? null),
]);
}
Also, in ReadersFormRequest when you inject the other request or resolve it from the container it doesn't work correctly, so it is better to get the rules like that:
public function rules()
{
$mediaRules = (new SocialMediaFormRequest())->rules();
and in order to access the media.* attributes in ReadersFormRequest you can again use prepareForValidation:
public function prepareForValidation()
{
$this->merge([
'url' => $this->media['url'] ?? null,
'title' => $this->media['title'] ?? null,
]);
}

How to send a mutation on testing in Laravel using (Lighthouse package)

on this post the author made an example helper like this
and the package being used is the Lighthouse
public function graphql(string $query)
{
return $this->post('/graphql', [
'query' => $query
]);
}
and so can be used it like this:
$response = $this->graphql("{articles(first: 10) { edges { node { title } } } }");
but can I implement that on mutation? if example I have a mutation:
type Mutation {
sampleMutation(
id: ID!
)
}
I'm not sure how to do that on a mutation.
It is possible to test mutations.
In the newest version of Lighthouse you now have a trait you can add to your test class called MakesGraphQLRequests.
Taken from their docs is the following example on how to create a test for a mutation.
public function testCreatePost(): void
{
/** #var \Illuminate\Foundation\Testing\TestResponse $response */
$response = $this->postGraphQL([
'query' => '
mutation CreatePost($title: String!) {
createPost(title: $title) {
id
}
}
',
'variables' => [
'title' => 'Automatic testing proven to reduce stress levels in developers'
],
]);
}
The response object you get will contain the json result like from your mutation. So here you can just make any json assertions as you normally would with Laravel.
In their documentation there is some examples on how validation on the json could look like.
Disclaimer: I am the writer of the refered article.

Laravel Dingo nested transformers

I'm trying to get one to many relationship objects with transformers. I want to get include metas but i only get just regular transform fields.
my transformer:
class AssistantTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'assistantmetas'
];
public function transform(User $user)
{
return [
'id' => (int) $user->id,
'firstname' => ucfirst($user->first_name),
'lastname' => ucfirst($user->last_name),
];
}
public function includeMetas(User $user)
{
$assistantmetas = $user->userMetas;
return $this->item($assistantmetas, new AssistantsMetaTransformer);
}
}
Just use defaultIncludes not available includes, because it needs to send request via url? include=assistantmetas to get result like this.

URL friendly in routes

I'm creating url friendly in my app, but it's not working, the app is giving me some issues related with "-".
It's giving me an error of:
ErrorException in PostController.php line 60:
Trying to get property of non-object
My ideal URL is:
http://domain.com/CATEGORY-title-of-post-ID
My route is:
Route::get('{category}-{title}-{id}', 'PostController#show');
PostController show function:
public function show($category,$title,$id)
{
$post = Post::find($id);
$user = Auth::user();
$comments = Comment::where('post_id',$id)
->where('approved',1)
->get();
return view('posts.show',compact('post','comments','user'));
}
Blade View:
<?php
$title_seo = str_slug($feature->title, '-');
?>
<a href="{{url($feature->categories[0]->internal_name."-".$title_seo."-".$feature->id)}}" rel="bookmark">
...</a>
There's a library called Eloquent-Sluggable that will create a unique slug for each post and correctly URL encode it.
To install (taken from the docs):
composer require cviebrock/eloquent-sluggable:^4.1
Then, update config/app.php by adding an entry for the service provider.
'providers' => [
// ...
Cviebrock\EloquentSluggable\ServiceProvider::class,
];
Finally, from the command line again, publish the default configuration file:
php artisan vendor:publish --provider="Cviebrock\EloquentSluggable\ServiceProvider"
To use, add the Sluggable trait to your model:
use Cviebrock\EloquentSluggable\Sluggable;
class Post extends Model
{
use Sluggable;
/**
* Return the sluggable configuration array for this model.
*
* #return array
*/
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
}
When you save an instance of your model, the library will automatically create a slug and save it to the newly created slug column of your model's table. So to access the slug you'd use $model->slug
To achieve your desired slug, rather than create it from title set by default. You can pass the source attribute of the sluggable method an array of field names, using a dot notation to access the attributes of a related model, like so:
public function sluggable()
{
return [
'slug' => [
'source' => ['category.name','title','id']
]
];
}
}
Why are you genering your "friendly URL" manually?
You have route helper function that builds for you a URL based on the given parameters.
Route::get('{category}-{title}-{id}', [
'as => 'post.show',
'uses' => 'PostController#show'
]);
echo route('post.show', ['testing', 'title', 'id']); // http://domain.dev/testing-title-id
This is not the best approach to implement SEO friendly URLs, anyway.
In your controller you ALWAYS use your ID to find a post, that means that category and title are completely useless to determine which resource needs to be served to the user.
You can make your life easier by doing something like:
Route::get('{id}-{slug}', [
'as => 'post.show',
'uses' => 'PostController#show'
]);
echo route('post.show', ['id', 'slug']); // http://domain.dev/id-slug
In your model you create an helper function that generates the slug for your post:
class Post
{
[...]
public function slug()
{
return str_slug("{$this->category}-{$this->title}");
}
}
Then, in your controller you need to check that the slug used to access the article is correct or not, since you don't want Google to index post with wrong slugs. You essentially force a URL to be in a certain way, and you don't lose index points.
class PostController
{
[...]
public function show($id, $slug)
{
$post = Post::findOrFail($id);
$user = Auth::user();
if ($post->slug() !== $slug) {
return redirect()->route('posts.show', ['id' => 1, 'slug' => $post->slug()]);
}
$comments = Comment::where('post_id', $id)->where('approved', 1)->get();
return view('posts.show', compact('post', 'comments', 'user'));
}
}

Yii2- get from related table name to add to Sluggable

Yii2. How can i get data from the related table and add it to Sluggable behavior.
In the example below, I want for every book add slug-title like "Book House, author Greenberg".
class Books extends \yii\db\ActiveRecord
{
public function behaviors()
{
return [
[
'class' => SluggableBehavior::className(),
'attribute' => "Book" . $this->name . ", author " . $this->getAuthor->name,
//'slugAttribute' => 'slug',
],
];
}
public function getAuthor()
{
return $this->hasOne(Author::className(), ['id' => 'author_id']);
}
}
Simply use a callback for the sluggable behavior like so:
public function behaviors()
{
return [
[
'class'=>SluggableBehavior::className(),
'value'=>function ($event) {
$parts = ['Book', $this->name, 'author', $this->author->name];
return Inflector::slug(implode('-', $parts));
},
],
];
}
My example will output a slug like this: book-house-author-greenberg which is more best practice for slugs than your version. Anyway...if you still prefer the way you described above, simply change the return value.
The doc for the feature used in my example is here.

Resources