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);