Best practice for adding custom logic in Lumen/Laravel controller - php

I'm using Lumen (Laravel) to create an API for an online activity/campaign application which handles things like on-site registrations and gift redemptions for various events. Occasionally, there are certain events with very specific functionality required which need their own custom logic. I'm wondering how to best handle this custom code from an architecture/best practices standpoint.
Here's what I have: I have a route which calls a CustomCampaignController like so:
$router->group([
'prefix' => 'v1'
], function () use ($router) {
// ..... other routes for standard activities
$router->post('customCampaigns', 'CustomCampaignController#runController');
});
Under App\Http\Controllers I've opened a directory to store classes for all custom activities. The customCampaigns route takes an activityId parameter whose value matches one of the activity classes. For example if the client posts activityId="MyCustomActivity" to customCampaigns, I would instantiate the following class: App\Http\Controllers\Custom\MyExampleActivity.
// app/Http/Controllers/CampaignController.php
public function runController(Request $request) {
$className = 'App\\Http\\Controllers\\Custom\\' . $request->input('activityId');
$customController = new $className;
return $customController->run();
}
The custom controller would then do its thing and return the response
// app/Http/Controllers/Custom/MyCustomActivity.php
namespace App\Http\Controllers\Custom;
class MyCustomActivity
{
public function __construct()
{
//
}
public function run()
{
// Custom logic here
return response('Response');
}
}
Is this a good approach or could it be considered an anti-pattern? Please let me know if there is another pattern for this type of problem.

I would prefer put the custom activity as the part of url. So, you will have something like this
$router->group([
'namespace' => 'App\Http\Controllers\Custom',
'prefix' => 'v1/customCampaigns'
], function () use ($router) {
$router->post('myCustomActivity', 'MyCustomActivityController#methodName');
});
Using this format, you can map the endpoint directly into a specific controller.

Related

Laravel 6 - Pass a parameter to $app->when()->needs()->give()

I am trying to use this package to push notifications to users via OneSignal. However I needed to make a little change. My API serves two (related) apps and I have two OneSignal configs. I am trying to override its ServiceProvider (using this technique).
The ServiceProvider presents itself as follows
<?php
namespace NotificationChannels\OneSignal;
use Berkayk\OneSignal\OneSignalClient;
use Illuminate\Support\ServiceProvider;
use NotificationChannels\OneSignal\Exceptions\InvalidConfiguration;
class OneSignalServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
$this->app->when(OneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
}
}
The behavior that I want to change is located in the line
$oneSignalConfig = config('services.onesignal');
As it assumes that my config/services.php has the following entry (stated in the doc) :
// config/services.php
...
'onesignal' => [
'app_id' => env('ONESIGNAL_APP_ID'),
'rest_api_key' => env('ONESIGNAL_REST_API_KEY')
],
...
Whereas I want to set my config/services.php as follows
// config/services.php
...
'onesignal' => [
'app1' => [
'app_id' => env('ONESIGNAL_1_APP_ID'),
'rest_api_key' => env('ONESIGNAL_1_REST_API_KEY')
],
'app2' => [
'app_id' => env('ONESIGNAL_2_APP_ID'),
'rest_api_key' => env('ONESIGNAL_2_REST_API_KEY')
],
],
...
And I want somehow to tell my ServiceProvider (through some kind of parameter) to either do
$oneSignalConfig = config('services.onesignal.app1');
OR
$oneSignalConfig = config('services.onesignal.app2');
But I didn't find any way to pass a parameter to the class, the boot function or the give method (and if I understood well I shouldn't even be doing that).
The only way I could think of is to create two classes that extend the OneSignalChannel::class
and duplicate code in the boot function so it becomes as follows :
public function boot()
{
$this->app->when(FirstOneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal.app1');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
$this->app->when(SecondOneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal.app2');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
}
The difference in the when provoking a difference in the config but it seems a lot of duplication and not extensible (what if I had three apps).
Should I use this method, or is there a way to pass a parameter to this ServiceProvider or is there another solution ?
https://stackoverflow.com/a/34224082/10371024
I could see what you need, but to pass parameter to boot method is not a good idea according to Laravel architecture. You may try to get what you want with using events as Vladislav suggested.

Implement authorization in Laravel using external model

I want to protect my routes with authorization levels in laravel - client users have access to one group while admin gets access to a separate group. However, the laravel API I'm creating runs concurrent to the existing legacy app which uses its own Users class, rather than the pre rolled eloquent Users model that all the docs use.
So far I haven't been able to determine how best to create this custom authorization middleware.
Basically I'd like to do something like this:
Route::group(['middleware' => 'custom_auth', function() {
Route::get('/' function() {
return "Hello World";
}
}];
//where 'custom_auth' points to something like
function isAdmin() {
if (Core\User->check_is_admin()) {
return true;
} else {
return false;
}
}
Kind of an open-ended question, I know, so any links to blogs/docs/videos would be appreciated. Thanks!
Solved:
I added custom middleware to App\Http\Middleware that copied the structure of the prebuilt Middleware\Authenticate.php file and uses the custom isAdmin() method.
Make sure to include in HTTP\Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth_admin' => \App\Http\Middleware\AuthenticateAdmin::class, // <- New Middleware
...]
Then in my Routes\admin_api
Route::group(['middleware' => 'auth_admin'], function () {
Route::get('/', function () {
return "Hello World";
});
This all works!

Yii2 RESTful: Content Negotiation languages not work

I implemented a structure of API Rest with the models in Yii2. Everything works great for actions (index, create, update etc ...) and methods (GET, POST, PUT etc ..) but I have a problem with the ContentNegotiator class.
Specifically, if I pass as a parameter of GET the language in which the response is to be translated, this is ignored.
According to the documentation for setting the language of response we need to set allowed languages of ContentNegotiator (look at my behaviors()) and make a request like this:
http://localhost/api/v1/users?_lang=it-IT
But the response continues to be in English. Why??? Nothing against the English =)
This is my ActiveController child class that extend from yii\rest\Controller.
use yii\rest\ActiveController;
use yii\filters\VerbFilter;
class AActiveController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['verbFilter'] = [
'class' => VerbFilter::className(),
'actions' => $this->verbs(),
];
$behaviors['contentNegotiator']['languages'] = [
'en-EN',
'it-IT',
'de-DE',
'ru-RU',
];
return $behaviors;
}
...
N.B.: I debug through yii\filters\ContentNegotiator class of the framework and at this point the app language is set correctly but the response is always in English.
negotiate() public method
public function negotiate()
{
$request = $this->request ?: Yii::$app->getRequest();
$response = $this->response ?: Yii::$app->getResponse();
if (!empty($this->formats)) {
$this->negotiateContentType($request, $response);
}
if (!empty($this->languages)) {
Yii::$app->language = $this->negotiateLanguage($request);
}
debug(Yii::$app->language); // result OK!: it-IT
}
Looks like some build-in errors not translated, for example yii\rest\Action:103 throws throw new NotFoundHttpException("Object not found: $id") its not translated. You have different ways to solve this problem:
Good way. Extend this Action and throw correct exception with translate
Bad way. Edit framework file itself.
Best way. Create patch to framework and send to maintainers.
For more information about i18n see documentation.

Organizing Controllers in laravel

I recently dove into the world of laravel (version 5.4). While initially confused, the concept of MVC makes a lot of sense in writing large applications. Applications that you want to be easily understood by outside developers.
Using laravel for this has greatly simplified coding in PHP and has made the language fun again. However, beyond dividing code into its respective models, views, and controllers, what happens if we need to divide controllers to prevent them from growing too large?
A solution that I have found to this is to define one controller each folder and then fill that controller with traits that further add functionalities to the controller. (All-caps = folder):
CONTROLLER
HOME
Controller.php
TRAITS
additionalFunctionality1.php
additionalFunctionality2.php
additionalFunctionality3.php
...
ADMIN
Controller.php
TRAITS
additionalFunctionality1.php
additionalFunctionality2.php
additionalFunctionality3.php
...
Within routes/web.php I woud initialize everything as so:
Route::namespace('Home')->group(function () {
Route::get('home', 'Controller.php#loadPage');
Route::post('test', 'Controller.php#fun1');
Route::post('test2', 'Controller.php#fun2');
Route::post('test3', 'Controller.php#fun3');
});
Route::namespace('Admin')->group(function () {
Route::get('Admin', 'Controller.php#loadPage');
Route::post('test', 'Controller.php#fun1');
Route::post('test2', 'Controller.php#fun2');
Route::post('test3', 'Controller.php#fun3');
});
With me being new to laravel, this seems like a simple and elegant way to organize my logic. It is however something I do not see while researching laravel controller organization.
The Question
Is there an issue, both in the short-run and in the long-run, of organizing my data like this? What is a better alternative?
Example Controller:
<?php
namespace App\Http\Controllers\Message;
use DB;
use Auth;
use Request;
use FileHelper;
use App\Http\Controllers\Message\Traits\MessageTypes;
use App\Http\Controllers\Controller;
class MessageController extends Controller
{
// Traits that are used within the message controller
use FileHelper, MessageTypes;
/**
* #var array $data Everything about the message is stored here
*/
protected $data = []; // everything about the message
/**
* #var booloean/array $sendableData Additional data that is registered through the send function
*/
protected $sendableData = false;
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('access');
}
/**
* Enable sendableData by passing data to the variable
*
* #param array $data Addition data that needs to registrered
* #return MessageController
*/
protected function send ($data = []) {
// enable sendableData by passing data to the variable
$this->sendableData = $data;
return $this;
}
/**
* Enable sendableData by passing data to the variable
*
* #param string $type The type of message that we will serve to the view
* #return MessageController
*/
protected function serve ($type = "message") {
$this->ss();
$this->setData(array_merge($this->sendableData, $this->status[$type]));
$this->data->id = DB::table('messages')->insertGetId((array) $this->data);
}
/**
* Set the data of the message to be used to send or construct a message
* Note that this function turns "(array) $data" into "(object) $data"
*
* #param array $extend Override default settings
* #return MessageController
*/
protected function setData(array $extend = []) {
$defaults = [
"lobby" => Request::get('lobbyid'),
"type" => "text",
"subtype" => null,
"body" => null,
"time" => date("g:ia"),
"user" => Auth::User()->username,
"userid" => Auth::User()->id,
"day" => date("j"),
"month" => date("M"),
"timestamp" => time(),
"private" => Request::get('isPrivate') ? "1" : "0",
"name" => Request::get('displayname'),
"kicker" => null
];
$this->data = (object) array_merge($defaults, $extend);
// because a closure can not be saved in the database we will remove it after we need it
unset($this->data->message);
return $this;
}
/**
* Send out a response for PHP
*
* #return string
*/
public function build() {
if($this->data->type == "file") {
$filesize = #filesize("uploads/" . $this->data->lobby . "/" . $this->data->body);
$this->data->filesize = $this->human_filesize($filesize, 2);
}
// do not send unneccessary data
unset($this->data->body, $this->data->time, $this->data->kicker, $this->data->name, $this->data->timestamp);
return $this->data;
}
/**
* Send out a usable response for an AJAX request
*
* #return object
*/
public function json() {
return json_encode($this->build());
}
}
?>
Laravel architecture is simple enough for any size of the application.
Laravel provides several mechanisms for developers to tackle the fatty controllers in your Application.
Use Middlewares for authentications.
Use Requests for validations and manipulating data.
Use Policy for your aplication roles.
Use Repository for writing your database queries.
Use Transformers for your APIs to transform data.
It depends on your application. if it is too large and have different Modules or functionalities then you should use a modular approach.
A nice package is available for making independent modules here
Hope this helps.
I think you should do a little differently ! First you should use your traits at the same levels as the controllers since traits are not controllers, your tree should look more like :
Http
Controller
Controller.php
Home
YourControllers
Admin
Your admin controllers
Traits
Your Traits
Next your routes need to be more like that :
Route::group(['prefix' => 'home'], function()
{
Route::get('/', 'Home\YourController#index')->name('home.index');
}
Route::group(['prefix' => 'admin', 'middleware' => ['admin']], function()
{
Route::get('/', 'Admin\DashboardController#index')->name('dashboard.index');
}
You can use many kink or routes like :
Route::post('/action', 'yourControllers#store')->name('controller.store');
Route::patch('/action', 'yourControllers#update')->name('controller.update');
Route::resource('/action', 'yourController');
The Resource route creates automatically the most used your, like post, patch, edit, index.. You just need to write the action and the controller called with its action. You can check out your toutes with this command : php artisan route:list
Laravel also has many automated features, like the creation of a controller with this command : php artisan make:controller YourController.
For the routes the prefix creates portions of url, for example all the routes inside the route group with the prefix 'admin' will lool like : www.yourwebsite.com/admin/theroute, and can also be blocked for some users with a middleware.
To get familiar with laravel i suggest you follow the laravel 5.4 tutorial from scratch by Jeffrey Way on Laracasts, he's awesome at explaining and showing how laravel works. Here is a link : https://laracasts.com/series/laravel-from-scratch-2017
Hope it helps, ask me if you want to know anything else or have some precisions, i'll try to answer you !

Unable to generate a URL

I am currently trying to create a link on the index page that'll allow users to create an item. My routes.php looks like
Route::controller('items', 'ItemController');
and my ItemController looks like
class ItemController extends BaseController
{
// create variable
protected $item;
// create constructor
public function __construct(Item $item)
{
$this->item = $item;
}
public function getIndex()
{
// return all the items
$items = $this->item->all();
return View::make('items.index', compact('items'));
}
public function getCreate()
{
return View::make('items.create');
}
public function postStore()
{
$input = Input::all();
// checks the input with the validator rules from the Item model
$v = Validator::make($input, Item::$rules);
if ($v->passes())
{
$this->items->create($input);
return Redirect::route('items.index');
}
return Redirect::route('items.create');
}
}
I have tried changing the getIndex() to just index() but then I get a controller method not found. So, that is why I am using getIndex().
I think I have set up my create controllers correctly but when I go to the items/create url I get a
Unable to generate a URL for the named route "items.store" as such route does not exist.
error. I have tried using just store() and getStore() instead of postStore() but I keep getting the same error.
Anybody know what the problem might be? I don't understand why the URL isn't being generated.
You are using Route::controller() which does generate route names as far as I know.
i.e. you are referring to "items.store" - that is a route name.
You should either;
Define all routes specifically (probably best - see this blog here)
Use Route::resource('items', 'ItemController'); see docs here
If you use Route::resource - then you'll need to change your controller names
The error tells you, that the route name is not defined:
Unable to generate a URL for the named route "items.store" as such route does not exist.
Have a look in the Laravel 4 Docs in the Named Routes section. There are several examples that'll make you clear how to use these kind of routes.
Also have a look at the RESTful Controllers section.
Here's an example for your question:
Route::get('items', array(
'as' => 'items.store',
'uses' => 'ItemController#getIndex',
));
As The Shift Exchange said, Route::controller() doesn't generate names, but you can do it using a third parameter:
Route::controller( 'items',
'ItemController',
[
'getIndex' => 'items.index',
'getCreate' => 'items.create',
'postStore' => 'items.store',
...
]
);

Categories