mmedia/classcontroller

A controller that can take standard PHP classes and convert them to controller methods with auto validation.

1.2.1 2022-07-14 08:14 UTC

This package is auto-updated.

Last update: 2024-05-14 16:46:50 UTC


README

Latest Version on Packagist Total Downloads Maintained Coverage

The ClassController extends the basic Controller and allows you to use defined PHP class methods directly as controller methods.

Installation

You can install the package via composer:

composer require mmedia/classcontroller

Usage

See the Test::class that this example is using.

use MMedia\ClassController\Http\Controllers\ClassController;

class TestClassController extends ClassController
{
    protected $inheritedClass = 'MMedia\ClassController\Examples\Test';

    //Done. All methods from the class Test are inherited and wrapped in Laravel validation automatically
}

When you extend a ClassController and give your new controller a name like {inheritedClass}ClassController, all of the methods of {inheritedClass} are inherited and wrapped with Laravel validation rules and responses. If you need a namespaced class, you can use inheritedClass property to specify a class with its namespace instead.

In your routes, you can now call all the methods of the inheritedClass, Test::class in this case, directly:

// We're just using the methods in the inherited class methods directly
Route::get('/noParams', [TestClassController::class, 'noParams']); // === \Test::noParams()
Route::get('/mixedParam/{param}', [TestClassController::class, 'mixedParam']); // === \Test::mixedParam($param) + auto validation!
Here is the equivalent when extending the default Controller instead In a `ClassController`, all this code is auto handled for you!
<?php

namespace App\Http\Controllers\Api\Test;

use Illuminate\Http\Request;

class TestController extends Controller
{
  public function noParams(Request $request)
    {
        $testClass = new Test();

        try {
            $methodResult = $testClass->noParams();
            if ($request->wantsJson()) {
                return response()->json($methodResult, 200);
            }
            return back()->with('success', $methodResult);
        } catch (\Exception $e) {
            return abort(400, $e->getMessage());
        }
    }

    public function mixedParam(Request $request)
    {
        $validatedData = $request->validate([
            'param' => ['required', 'integer'],
        ]);

        $testClass = new Test();

        try {
          $methodResult = $testClass->mixedParam($validatedData['param']);
            if ($request->wantsJson()) {
                return response()->json($methodResult, 200);
            }
            return back()->with('success', $methodResult);
        } catch (\Exception $e) {
            return abort(400, $e->getMessage());
        }
    }
}

Defining parameters to pass to the inheritedClass constructor

Sometimes a class requires some parameters in its constructor. To define these parameters, your controller should implement the classParameters method, and return the required parameters in the correct order as an iterable.

protected function classParameters(): iterable
{
    return [$param1, $param2];
}

Further setting up the class after instantiation

If you need to do more on the class before a method is called but after it is set up and instantiated, you can implement the postClassSetup method, where you have access to the instance of your class as $this->class(). This method should not return anything.

protected function postClassSetup(): void
{
    // Your code here. You have access to $this->class().
}

Overriding a specific method

If you need to override the behaviour of a specific method, you can simply define it in your controller using the method name that you want to override. Using the original example of Test::class:

public function mixedParam(Request $request)
{
    // You can write your own $request->validate(), or use the one from ClassController which validates that the data passed to the original class method is correct
    $validatedData = $this->getValidatedData($this->class(), 'mixedParam');

    // Call the original method if you want, or override it completely
    return $this->class()->mixedParam($validatedData['param']);
}

Note that while you can write your own validation logic, here we chose to use the already existing method getValidatedData() that is provided by the ValidatesClassMethods trait - the method takes a class name and method name as parameters and then validates all the required method parameters.

Creating a ClassController with Laravel Artisan

First, make sure you publish the stubs with the following command below:

php artisan vendor:publish --tag=classcontroller-stubs

You can then use the Artisan command to create ClassControllers whenever you want. Specify the ---type=class option, followed by the class name you are inheriting, to create a new ClassController:

php artisan make:controller --type=class TestClassController

In detail

Responses

The ClassController will respond differently depending on your headers. If you pass the Accept:application/json header, the responses will be in JSON. If not, you will be redirected back to the previous page with either a validation error if one is thrown, or a success with the result of the method called.

Validation

Validation rules are built using the class method parameters. For example, a function test($param1, $param2) will build a validator that requires param1 and param2 to be passed in the request.

If the parameter has a default value, for example test($param1 = "defaultValue"), the validation rule for param1 will be nullable.

If the parameter is typed, for example test(int $param1), the validation rules for param1 will be required and integer.

If the parameter is variadic, for example test(...$param1), the validation rules for param1 will be required and array.

If the parameter is variadic and typed, for example test(int ...$param1), the validation rules for param1 will be required and array, and each element will also have a validation rule of type integer.

Exceptions

Validation exceptions

Validation exceptions will be handled natively by Laravel validator, and will return the errors in the errors key with a status code of 422. As an example, a validation exception may return something like:

{
  "errors": {
    "param": [
      "Param is required.",
      "Param must be an integer."
    ]
  }
}

Remember, you must pass the accept header in order to get JSON data back.

Method exceptions

If the called method throws an exception, it will be caught by the ClassController. If you have specified that you accept JSON in the response, the message will be returned as JSON with the key-value of {"message": "Error message"}, and a status code of 400. Otherwise, the Laravel 400 page will be shown.

ClassController exceptions

Exceptions may be thrown by the ClassController itself, especially if its not set up properly. As an example: if the inheritedClass could not be determined, you will receive a native PHP error with a clear message and a status code of 500.

All methods

classParameters(): iterable

Parameters passed to the inherited class constructor. See: defining parameters to pass to the class.

postClassSetup(): void

Method that runs after the class is instantiated. See: further setting up the class after instantiation.

getValidatedData($className, $methodName): array

Method that takes the name of a method and a class, looks through the method parameters, generates a Laravel validation instance and validates it using Laravel rules. Returns an array of valid data, or throws a ValidationException if there is a validation error. See: validation.

class(): object

Get an instance of the inherited class. The actual instance of the class, already instantiated.

All properties

inheritedClass

A protected string ('MyClass') or class (MyClass::class). This will be the class that the methods are inherited from.

If you do not define this property and your controller follows the naming standard of {inheritedClass}ClassController extends ClassController, the inheritedClass will be taken from your controllers name.

ValidatesClassMethods trait

If you want to use the validator method but don't want to extend your controller with the ClassController class (or want to use the validation logic outside of a controller), you can use the ValidatesClassMethods trait in your code.

use MMedia\ClassController\ValidatesClassMethods;

class myClass
{
    use ValidatesClassMethods;

  /**
   * Example method showing how to use the `getValidatedData` method.
   *
   * @param string $param1
   * @param int|null $param2
   * @throws ValidationException
   * @return string
   */
  public function test(string $param1, ?int $param2)
  {
      // Note: in the real world, it makes little sense to get validated data from within the same method as the method that is being called - PHP would have already thrown an error if the params were not valid types.
      $validatedData = $this->getValidatedData(get_class($this), __FUNCTION__);

      // Your code here. $validatedData['param1'] is a valid string and $validatedData['param2'] is a valid integer or null.

      return $validatedData['param1'] . $validatedData['param2'];
  }
}

Limitations

  • Untyped method parameters will not generate a specific type validation rule - see: Validation
  • Method parameters of type array will not validate each array element
  • Methods that don't specify parameters but instead use func_get_args() will not generate any validation rules
  • Authorisation is not implemented - however, you can authorise requests at the route level

Package development

Running

You can use the included VSCode devcontainer to develop within a PHP container.

Testing

composer test

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email contact@mmediagroup.fr instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.