How to perform additional tasks in Yii2 restful controller? - php

Here is how my RESTful controller looks like.
<?php
namespace backend\controllers;
use yii\rest\Controller;
use yii;
use yii\web\Response;
use yii\helpers\ArrayHelper;
class UserController extends \yii\rest\ActiveController
{
public function behaviors()
{
return ArrayHelper::merge(parent::behaviors(), [
[
'class' => 'yii\filters\ContentNegotiator',
'only' => ['view', 'index'], // in a controller
// if in a module, use the following IDs for user actions
// 'only' => ['user/view', 'user/index']
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
'languages' => [
'en',
'de',
],
],
[
'class' => \yii\filters\Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
],
],
]);
}
public $modelClass = 'backend\models\User';
public function actions()
{
}
public function sendMail(){
//Need to call this function on every create
//This should also have the information about the newly created user
}
}
It works very well with default behavior but it is not very practical that you will just create the user and exit. You need to send email with verification link SMS etc, may be update some other models based on this action.
I do not want to completely override the create method as it works well to save data and return back JSON.
I just want to extend its functionality by adding a callback kind of a function which can accept the newly created user and send an email to the person.

Take a look here: https://github.com/githubjeka/yii2-rest/blob/bf034d26f90faa3023e5831d1eb165854c5c7aaf/rest/versions/v1/controllers/PostController.php
As you can see this is using the prepareDataProvider to change the normal way the index action is using. This is very handy. You can find the definition of prepareDataProvider here: http://www.yiiframework.com/doc-2.0/yii-rest-indexaction.html#prepareDataProvider()-detail
Now as you can see there are 2 additional methods afterRun() and beforeRun() that are also available for the create action. http://www.yiiframework.com/doc-2.0/yii-rest-createaction.html
You may be able to use these 2 functions and declare them similar to prepareDataProvider to do more things like sending an email. I have not tried them myself but I believe that should be the way to go.

The easiest way would be getting benefit from afterSave() method in your model. This method will be called after each save process.
public function afterSave($insert, $changedAttributes) {
//calling a send mail function
return parent::afterSave($insert, $changedAttributes);
}
Another advantage of this method is the data you have stored in your object model. For example accessing email field:
public function afterSave($insert, $changedAttributes) {
//calling a send mail function
\app\helpers\EmailHelper::send($this->email);
return parent::afterSave($insert, $changedAttributes);
}
the value of $this->email is containing the saving value into database.
Note
You can benefit from $this->isNewRecord to detect whether the model is saving new record into database or updating an existing record. Take a look:
public function afterSave($insert, $changedAttributes) {
if($this->isNewRecord){
//calling a send mail function
\app\helpers\EmailHelper::send(**$this->email**);
}
return parent::afterSave($insert, $changedAttributes);
}
Now, it only sends mail if new record is being saved into database.
Please note that you can also benefit from Yii2's EVENTS.
As official Yii2's documentation:
This method is called at the end of inserting or updating a record.
The default implementation will trigger an EVENT_AFTER_INSERT event when $insert is true, or an EVENT_AFTER_UPDATE event if $insert is false. The event class used is yii\db\AfterSaveEvent. When overriding this method, make sure you call the parent implementation so that the event is triggered.

Related

PHPUnit how to write a Laravel Nova Observer test

I would like to write a test for my CommentObserver. This observer is only registered in the NovaServiceProvider but not the AppServiceProvider. This means I cannot test my observer by using my own Controllers.
In my eyes I have 3 ways to test my observer:
Either performing a feature test by sending a post request to the Nova API
Mocking the observer by calling the function in the observer to check if the function perfoms as desired
Trying to register my observer on the fly in the AppServiceProvider, performing a request and deregistering the observer in the AppServiceProvider again.
I tried to find a solution for any of these 3 ways to test my observer but unfortunately I faild with any of them.
Problems:
For way 1 I always get a validation error and Nova tells me that my input is invalid.
For way 2 I fail at mocking the observer function
For way 3 I didn't find any solution on how to register and deregister the oberserver on the fly at the AppServiceProvider
Do you guys have idea and solition on how I can test my CommentObserver (which is as written above only registered in my NovaServiceProvider).
Update:
So, here is the code of my observer. I need to have an valid request to test my observer in order to have the ability to access the $request->input('images') variable. I do know I can also use $comment->content instead of request()->input('content') because $comment->content already contains the new content which is not saved it this point.
The reason why I need a valid request is that the variable images is not part of the Comment model. So I cannot use $comment->images because it simply doesn't exist. That's why I need to access the request input. What my observer is basically doing is to extract the base64 images from the content, saves them to the server and replaces them by an image link.
class CommentObserver
{
public function updating(Comment $comment)
{
if (!request()->input('content')) {
return;
}
if (request()->input('content') == $comment->getRawOriginal('content')) {
return;
}
$images = request()->input('images');
if(!is_array($images)) {
$images = json_decode(request()->input('images'));
}
checkExistingImagesAndDeleteWhenNotFound($comment, request()->input('content'), 'comments', 'medium');
$comment->content = addBase64ImagesToModelFromContent($comment, request()->input('content'), $images, 'comments', 'medium');
}
}
This is my test so far. I choose way 1 but as described already this always leads to an validation error by the nova controller and I cannot figure out what is the error/what is missing or wrong.
class CommentObserverTest extends TestCase
{
/** #test */
public function it_test()
{
$user = User::factory()->create([
'role_id' => Role::getIdByName('admin')
]);
$product = Product::factory()->create();
$comment = Comment::factory()->create(['user_id' => $user->id, 'content' => '<p>Das ist wirklich ein super Preis!</p>', 'commentable_type' => 'App\Models\Product', 'commentable_id' => $product->id]);
$data = [
'content' => '<p>Das ist wirklich ein HAMMER Preis!</p>',
'contentDraftId' => '278350e2-1b6b-4009-b4a5-05b92aedaae6',
'pageStatus' => PageStatus::getIdByStatus('publish'),
'pageStatus_trashed' => false,
'commentable' => $product->id,
'commentable_type' => 'App\Models\Product',
'commentable_trashed' => false,
'user' => $user->id,
'user_trashed' => false,
'_method' => 'PUT',
'_retrieved_at' => now()
];
$this->actingAs($user);
$response = $this->put('http://nova.mywebsiteproject.test/nova-api/comments/' . $comment->id, $data);
dd($response->decodeResponseJson());
$das = new CommentObserver();
}
}
Kind regards and thank you
Why depend on the boot method in your NovaServiceProvider? It is possible to call the observe() method on the fly in your test:
class ExampleTest extends TestCase
{
/** #test */
public function observe_test()
{
Model::observe(ModelObserver::class);
// If you need the request helper, you can add input like so:
request()->merge([
'content' => 'test'
]);
// Fire model event by updating model
$model->update([
'someField' => 'someValue',
]);
// Updating should be triggered in ModelObserver
}
}
It should be now be possible in your observer class:
public function updating(Model $model)
{
dd(request()->input('content')); // returns 'test'
}

Api-Platform User Login via GraphQL

I am creating an API using API-Platform and have set up my user entity etc using the standard symfony security bundle (https://symfony.com/doc/current/security.html#retrieving-the-user-object)
I have the login working with REST at {url}/api/login using JWT but I cannot see any way of sending my login details with GraphQL
The API-platform documentation shows how to set up security and how to setup GraphQL separately but doesn't really show how to combine them.
https://api-platform.com/docs/core/graphql
https://api-platform.com/docs/core/fosuser-bundle
How do I make the login accessible in GraphQL?
Currently, I only have the createUser updateUser and deleteUser mutations, I assume I would need an authenticateUser one?
Yes, you'll need a custom mutation for the login.
Assuming you are using the API Platform standard docs for the API, you are using JWT to authenticate your calls, you need a UserMutationResolver for auth:
<?php
namespace App\Resolver;
use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Doctrine\ORM\EntityManagerInterface;
final class UserMutationResolver implements MutationResolverInterface
{
public function __construct(
private UserPasswordEncoderInterface $userPasswordEncoder,
private JWTTokenManagerInterface $JWTManager,
private EntityManagerInterface $em,
)
{}
/**
* #param User|null $item
*
* #return User
*/
public function __invoke($item, array $context)
{
// Mutation input arguments are in $context['args']['input'].
if ($context["info"]->fieldName == 'loginUser') {
$userRepository = $this->em->getRepository("App:User");
$user = $userRepository->findOneBy(['email' => $item->getEmail()]);
if ($this->userPasswordEncoder->isPasswordValid($user, $item->getPlainPassword())) {
$token = $this->JWTManager->create($item);
$user->setToken($token);
}
return $user;
}
}
}
Then you add that custom mutation to the User entity. Be sure to add the names of the auto-generated mutations/queries or they will disappear (item_query, create, update, delete, collection_query). You'll also need to disable some of the stages, since this is a mutation Api Platform will try and save this as a new user, which we don't want, so as you see below, 'write' => false and 'validate' => false
// api/src/Entity/User.php
// imports etc .
// ...
#[ApiResource(
normalizationContext: ["groups" => "user:read"],
denormalizationContext: ["groups" => "user:write"],
attributes: [
'write' => true,
],
graphql: [
'item_query',
'create',
'update',
'delete',
'collection_query',
'login' => [
'mutation' => UserMutationResolver::class,
'write' => false,
'validate' => false,
'args' => [
'email' => ['type' => 'String!', 'description'=> 'Email of the user ' ],
'password' => ['type' => 'String!', 'description'=> 'Password of the user ' ]
]
],
],
iri:"http://schema.org/Person",
)]
#[UniqueEntity(fields: ["email"])]
class User implements UserInterface
{
// ...
This will create a mutation that you can access like this:
mutation {
loginUser(input: {email:"test1#test.com", password:"password"}) {
user {
token
}
}
}
and the result should look something like this:
{
"data": {
"loginUser": {
"user": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2MTgzNjM1NDQsImV4cCI6MTYxODM2NzE0NCwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoidGVzdDFAdGVzdC5jb20ifQ.pSoAyNcaPa4MH2cxaAM4LJOGvirHfr94GMf_k20eXlF1LAaJyXRraKyC9hmBeKSUeAdIgowlfGFAHt96Z4EruBlkn2mbs3mj3uBWr2zqfNTVyQcicJDkJCO5EpbpexyLO5igD9qZU__4ctPvZcfWY-dJswSfiCTP1Uz0BiGFsGqb72chd8Rhn5Btls-D6b9Uuzl9ZZeLj2pIuBA-yi_CMm3CzopKIJ1NySMT8HyvafHcTdfpzFWFPoUqxkVAzt4U6tqBpEnTqmwRW_3kTisJhIY9xH2uXKghz2VWM6mvTL1PahZgbwLqsVb_sBOOEtiASpGf8WNc1uXtKNhBCb_YJw"
}
}
}
}
I cannot see any way of sending my login details with GraphQL
Auth protected queries should be sent with Authorization header. Exact method depends on client-side technology, f.e. Apollo client supports this by middleware.
You can use existing REST login endpoint (fetch/get token) or create login mutation - example.
Another inspiration can come from a more complex example apollo-universal-starter-kit

How to check for required fields in object?

i have example object with fields
name => John
surname => Dow
job => engineer
and output form with placeholders. some required, some not.
what is best practice for check if it requred and show error with null fields?
There are multiple ways actually you can do that inside of controller method or make use of Laravels Request Classes for me I prefer to use Request Classes
look below I will list the two examples
Validate inside the controller's method
public function test(Request $request){
if($request->filled('name){
/*filled will check that name is set on the current
request and not empty*/
//Do your logic here
}
}
Second way is by using the Validator Facade inside your controller
use Validator;
class TestController{
public function test(Request $request){
$validator = Validator::make($request->all(), [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
/*continue with your logic here if the request failed on
validator test Laravel will automatically redirect back
with errors*/
}
}
Third way my favorite one personally
you can generate a Request class using this command
php artisan make:request AddBookRequest
that will generate the request class under "app/Http/Requests/AddBookRequest" , inside of any generated request class you will find two methods authorize() and rules()
in the authorized method you have to return truthy or falsy value this will detect if the current user making the request has authorization to fire this request inside of the rules method you do pretty much as you did in the Validator in the second way check the example
public function authorize(){
return true;
}
public function rules(){
return [
'title' => 'required|string',
'author_id' => 'required|integer'
];
}
then simply in your controller you can use the generated request like this
use App\Http\Requests\AddBookRequest;
public function store(AddBookRequest $request){
/* do your logic here since we uses a request class if it fails
then redirect back with errors will be automatically returned*/
}
Hope this helps you can read more about validation at
https://laravel.com/docs/5.6/validation
I think "simple is the best", just through object and check if properties exists
Ref: property_exists
Example:
if (property_exists($object, 'name')) {
//...do something for exists property
} else {
//...else
}

Prestashop - REST endpoints for my module

I'm developing Prestashop module, it will export customer data and orders, it will contain hooks for customer synchronization, cart and order events - generally module which will be an integration with CRM-like service.
My module contains it's own views, made in vue.js - single page, async. There are register, login, settings, etc. pages. Communication with backend is made by GET/POST requests on {baseUrl}/mymodule/actionname routes and simple json responses which vue views depend on. Simply I need to create REST endpoints for my module, something like examples below.
Wordpress custom RestApi:
class RestApi
{
public function __construct()
{
add_action('rest_api_init', array(get_class($this),
'register_endpoints'));
}
public static function register_endpoints()
{
register_rest_route('mymodule', '/login', array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array('RestApi', 'login' ),
));
}
}
SugarCRM custom RestApi:
class ModuleRestApi extends SugarApi
{
public function registerApiRest()
{
return [
'moduleLogin' => [
'reqType' => 'POST',
'noLoginRequired' => true,
'path' => [
'mymodule', 'login'
],
'method' => 'login'
],
];
}
}
I cannot find similar solution in PrestaShop, there is no word about custom endpoints in presta docs, I tried to use FrontModuleControllers with friendly url's but it doesn't seem to work for me, it throws a lot of stuff in response which is useless for me and when I try to override init() method it requires a lot of stuff too to actually initiate the controller. I need simple REST solution where I can put logic for recieving data from my views, pass it to my CRM service and return json responses to my views. I don't need any more templates or views rendering, just routing for cummunication.
PrestaShop doesn't support this out of the box. You can however do it with a module and front controllers.
This is a basic example of doing it.
1. Module to register friendly URLs
class RestApiModule extends Module
{
public function __construct()
{
$this->name = 'restapimodule';
$this->tab = 'front_office_features';
$this->version = '1.0';
parent::__construct();
}
public function install()
{
return parent::install() && $this->registerHook('moduleRoutes');
}
public function hookModuleRoutes()
{
return [
'module-restapimodule-login' => [
'rule' => 'restapimodule/login',
'keywords' => [],
'controller' => 'login',
'params' => [
'fc' => 'module',
'module' => 'restapimodule'
]
]
];
}
}
2. Create an abstract REST controller
Create an abstract controller so that actual endpoints can extend from it. Create it in your module controllers folder lets name it AbstractRestController.php
abstract class AbstractRestController extends ModuleFrontController
{
public function init()
{
parent::init();
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
$this->processGetRequest();
break;
case 'POST':
$this->processPostRequest();
break;
case 'PATCH': // you can also separate these into their own methods
case 'PUT':
$this->processPutRequest();
break;
case 'DELETE':
$this->processDeleteRequest();
break;
default:
// throw some error or whatever
}
}
abstract protected function processGetRequest();
abstract protected function processPostRequest();
abstract protected function processPutRequest();
abstract protected function processDeleteRequest();
}
3. Create an actual front controller
Create the front controller in your module controllers/front folder and name it login.php.
require_once __DIR__ . '/../AbstractRestController.php';
class RestApiModuleLoginModuleFrontController extends AbstractRestController
{
protected function processGetRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'get'
]));
}
protected function processPostRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'post'
]));
}
protected function processPutRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'put'
]));
}
protected function processDeleteRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'delete'
]));
}
}
Install the module and now you can hit http://example.com/restapimodule/login and depending on the request type it's going to do whatever you want and you get back JSON response.
To add more endpoints add another module-restapimodule-endpointname entry into hookModuleRoutes array and a front controller that extends from AbstractRestController.
If you also want proper response codes etc. you're going to have to set headers with native php functions as PrestaShop afaik doesn't have any utilities to do it for you or use some kind of library.
Same goes for any other headers you might want to set such as content-type (by default it is text/html).
It is possible to use the Prestashop Webservice, that allows to add resources from modules. This solution could save some time in terms of standards and security.
The documentation regarding module resources in Prestashop Webservice is in this link:
https://webkul.com/blog/creating-prestashop-module-webservice-api/

Laravel 5.1 - Custom validation language file

Is it possible to conditionally set a custom language file (e.g. resources/lang/en/validation_ajax.php) for a validation request? Just to be clear, I don't want to change the app language, just use another set of messages depending on the request origin.
When I make an ajax validation call I want to use different messages since I'm showing the error messages below the field itself. So there's no need to show the field name (label) again.
I know you can define labels on 'attributes' => [] but it's not worth the effort since I have so many fields in several languages.
I'm using a FormRequest (there's no manual call on the Controller just a type hint).
You can override the messages() method for a specific request (let's say login request). Let me show you: At first place, you need yo create a new custom Form Request, here we will define a custom message for email.required rule:
<?php namespace App\MyPackage\Requests;
use App\Http\Requests\Request;
class LoginRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
public function messages()
{
return [
'email.required' => 'how about the email?',
];
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => ['required', 'email'],
'password' => ['required', 'confirmed']
];
}
}
Only email.required rule message will be override. For password it will display the default message set at validation.php file.
Now, apply the form request at your controller function like a type hint:
class LoginController{
public function validateCredentials(LoginRequest $request){
// do tasks here if rules were success
}
}
And that is all. The messages() method is useful if you need are creating custom packages and you want to add/edit validation messages.
Update
If you need to carry the bag of messages on into your package's lang file then you can make the following changes:
At your package create your custom lang file:
MyPackage/resources/lang/en/validation.php
Add the messages keeping the same array's structure as project/resources/lang/en/validation.php file:
<?php
return [
'email' => [
'required' => 'how about the email?',
'email' => 'how about the email format?',
],
];
Finally, at your messages() method call the lang's line of your package respectively:
public function messages(){
return [
'email.required' => trans('myPackage::validation.email.required'),
'email.emial' => trans('myPackage::validation.email.valid'),
];
}

Categories