Provider

Abstraction & Decoupling

In order to inject dependencies like a database wrapper instead of coupling models with a particular orm or database type, you can use providers and respective interfaces while programming. Providers are not "untouchable" and are therefore not located in the core directory (_neoan). The default package currently contains the following providers

  • Attributes (for PHP8 attribute hooks)
  • Auth (for JWT & Session injection)
  • Filesystem (mocking of native PHP file functions)
  • Model (Attribute hook & compliant Model interface)
  • MySql (MySql database wrapper, transformer & mocking)

Using MySql database providers as an example, let's see how providers can be used:

In a frame


        <?php


        namespace Neoan3\Frame;

        use Neoan3\Core\Serve;
        use Neoan3\Provider\MySql\Database;
        use Neoan3\Provider\MySql\DatabaseWrapper;


        /**
         * Class Demo
         * @package Neoan3\Frame
         */
        class Demo extends Serve
        {
            /**
             * @var Database|DatabaseWrapper
             */
            public DatabaseWrapper $db;

            /**
             * Demo constructor.
             * Optionally receives a provider
             * @param Database|null $db
             */
            function __construct(Database $db = null)
            {
                parent::__construct();
                // assignProvider takes three arguments:
                // the first one the reference name, then potential injections, the the fallback (default) assignment closure

                // since version 3.2 default injections can return directly
                $this->db = $this->assignProvider('db', $db, function(){
                    $credentials = getCredentials();
                    if(isset($credentials['your-db'])){
                        return new DatabaseWrapper($credentials['your-db']);
                    }
                });

                // neoan3 prior 3.2
                $this->assignProvider('db', $db, function(){
                    $credentials = getCredentials();
                    if(isset($credentials['your-db'])){
                        $this->provider['db'] = new DatabaseWrapper($credentials['your-db']);
                    }
                });
            }


        }
    

In a controller written singleton-style


        <?php


        namespace Neoan3\Component\Demo;

        use Neoan3\Core\Unicore;
        use Neoan3\Provider\MySql\Database;
        use Neoan3\Model\PostModel;

        class DemoController extends Unicore
        {
            /**
             * @var Database|null
             */
            private ?DataBase $db;


            /**
             * Demo constructor.
             * If you want to inject or decouple a component (or test it with mocking), you can create a constructor
             * that accepts your injections (here the database for model functionality)
             * @param Database|null $db
             */
            public function __construct(DataBase $db = null)
            {
                $this->db = $db;
            }

            /**
             * Route call (Singleton style)
             */
            function init()
            {
                $this
                    // register providers in the right order BEFORE initializing uni()
                    ->registerProvider($this->db)
                    // initialize Unicore singleton with wanted frame
                    ->uni('Demo')
                    // in this example, we are writing render parameters to all hooks (including "main")
                    ->addRenderParameter('posts', function($context){
                        // We now attach all found model entities to "posts"
                        return $context->loadModel(PostModel::class)::find([]);
                    })
                    /*
                        Demo/demo.view.html could look like this:
                        <section>
                            <p n-for="posts as post">{{post.title}}</p>
                        </section>
                    */
                    ->hook('main', 'demo')
                    ->output();
            }

        }
    

In a controller extending a frame


        <?php


        namespace Neoan3\Component\Demo;
        use Neoan3\Model\PostModel;
        use Neoan3\Frame\Demo as DemoFrame;

        class DemoController extends DemoFrame
        {
            /**
             * Route call
             */
            function init()
            {
                $this
                    // this time, let's bind our posts directly to the main hook only
                    ->hook('main', 'demo', [
                        'posts' => $this->loadModel(PostModel::class)::find(['^delete_date']);
                    ])
                    ->output();
            }

        }
    

In a controller extending a frame using PHP8 attributes


        <?php


        namespace Neoan3\Component\Demo;
        use Neoan3\Model\PostModel;
        use Neoan3\Frame\Demo as DemoFrame;

        class DemoController extends DemoFrame
        {
            /**
             * Route call
             */
            #[InitModel(DevModel::class)]
            function init()
            {
                $this
                    // when using PHP8 & a frame supporting the UseAttributes provider,
                    // conversing with a model is even cleaner without giving up DI & decoupling
                    ->hook('main', 'demo', [
                        'posts' => PostModel::find(['^delete_date']);
                    ])
                    ->output();
            }
        }
    

In a model


        <?php

        namespace Neoan3\Model;


        use Neoan3\Provider\MySql\Database;
        use Neoan3\Provider\MySql\Transform;

        /**
         * Class post
         * @method static get(string|null $id)
         * @method static create(array $modelArray)
         * @method static update(array $modelArray)
         * @method static find(array|null $conditionArray)
         * @package Neoan3\Model
         */
        class PostModel extends IndexModel
        {
            /**
             * @var Database|null
             */
            private static ?Database $db = null;

            /**
             * @param Database $database
             */
            static function init(Database $database)
            {
                self::$db = $database;
            }

            /**
             * @param $method
             * @param $args
             * @return mixed
             */
            public static function __callStatic($method, $args)
            {
                if(!method_exists(self::class, $method)){
                    $transform = new Transform('post', self::$db);
                    return $transform->$method(...$args);
                } else {
                    return self::$method(...$args);
                }
            }


        }
    

Testing

Now we can test database transactions and easily mock the outcome of queries. The cli-tool generates tests accordingly, so we will only focus on the injection logic


        ...
        // testing a model
        $mockDb = new Neoan3\Provider\MySql\MockDatabaseWrapper();
        $model = $mockDb->mockGet('Post');
        PostModel::init($mockDb);
        $toTest = PostModel::get($model['id']);
        $this->assertSame($model, $toTest);