Hey there, this is the more detailed version of the Quick Start found in the README file.
If you are not familiar with dependency injection containers read the Core Concepts.
Of course how you implement the service container is completely up to you but you should at least decide if you want to compile the dependency graph or not. It is possible to mix a compiled container with dynamic service definitions but for the love of consistent structuring things, you really should stick with one way.
You can read more about different types of implementations here:
Because the main difference between this and any other PHP service container is the meta language I will stick with it for this getting started guide.
Just like in the README the target directory structure will look like this:
app.php
app.ctn # this will be our container file.
config.ctn # this will be our configuration file.
composer.json
cache/ # make sure this is writable
src/
Human.php
SpaceShip.php
Engine.php
To construct a container instance we make use of the ContainerFactory
which will generate a PHP file containing our very own container.
$factory = new \ClanCats\Container\ContainerFactory(__DIR__ . '/cache');
$container = $factory->create('AppContainer', function($builder)
{
// create a new container file namespace and parse our `app.ctn` file.
$namespace = new \ClanCats\Container\ContainerNamespace([
'config' => __DIR__ . '/config.ctn',
]);
$namespace->parse(__DIR__ . '/app.ctn');
// import the namespace data into the builder
$builder->importNamespace($namespace);
});
And that's it, we are now able to modify the app.ctn
file which will build our Container instance.
Not informative enough? I'm sorry let me get a bit more into detail what happens here:
$factory = new \ClanCats\Container\ContainerFactory(__DIR__ . '/cache');
The container factory has not a lot of functionality its purpose is to write and read the generated PHP class. It supports a debug mode by simply setting the second argument to true
. In the debug mode the factory will ignore if a built file is already present and rebuild it every time.
$container = $factory->create('AppContainer', function($builder)
The create
method as you probably already guessed is where the container instance is being created. The first argument is the class name. Namespaces are supported so you could also set it to something like Acme\MainBundle\MainContainer
. As a second parameter, we have a callback where we define what the container should actually contain.
Note: Read more about this here: Container Factory & Container Builder
$namespace = new \ClanCats\Container\ContainerNamespace([
'config' => __DIR__ . '/config.ctn',
]);
Okay, so what the hell is a container namespace?
Well, look at it as a little application with its own file structure. The container namespace defines this file structure. config
is not a special key, it's just a name we assign to a file that should be accessible in your container files/scripts.
Now we have to parse the main file.
$namespace->parse(__DIR__ . '/app.ctn');
All parsed data (services, parameters) is now assigned to our namespace instance.
Read more about this here: Container Namespace
$builder->importNamespace($namespace);
Finally, we feed our namespace into the builder object.
Note: Before we continue here, you might want to check out Container File Syntax. There is also a tmLanguage
available for syntax highlighting support of ctn
files.
Parameters are always prefixed with a :
character and can be defined in any order. When defined inside of a ctn
file, they can hold scalar values and arrays. Technically the container has no limitation on what a parameter can contain, you can set a parameter containing anything you want manually with the setParamter
method.
Now in the app.ctn
define some parameter like this:
:firstname: = 'James'
:lastname: = 'Bond'
:code: 007
You can access the parameters of the container anytime:
echo $container->getParameter('firstname');
You might have noticed that in the setup there are two ctn
files mentioned. Let's go on and create the config.ctn
.
Inside the config.ctn
we define another parameter:
:missions.available: {
'Goldeneye',
'Goldfinger',
}
If we know would try to access missions.available
, we would get null
. That's because we need to import our config.ctn
into our main app.ctn
. doing so is simple:
import config
:firstname: = 'James'
:lastname: = 'Bond'
:code: 007
Remember where we constructed the container namespace? We defined the name of the config.ctn
to be simply config
.
This particular example (with first name, last name) might seem a bit useless. I like to separate configuration from the service definitions, using imports are a neat way to do so.
The first class we are going to create is the engine. This is class has nothing to do with the container itself, it purely acts as a demonstration.
The src/Engine.php
class is constructed with a given power and an amount of fuel. It can be throttled up for a given amount of time which will return the traveled distance and consume fuel. With the refuel
method the engine can be, well you probably already guessed it. Also, a mechanic can be assigned to the engine.
class Engine
{
protected $fuel;
protected $power;
protected $mechanic;
public function __construct(int $power, int $fuel) {
$this->power = $power;
$this->fuel = $fuel;
}
public function throttle(int $for) : int {
$this->fuel -= ($distance = $this->power * $for); return $distance;
}
public function getFuel() : int {
return $this->fuel;
}
public function refuel(int $amount) {
if ($this->mechanic) $this->fuel += $amount;
}
public function setMechanic(Human $mechanic) {
$this->mechanic = $mechanic;
}
}
So let's define our engine as a service.
@hyperdrive: Engine(500, 10000)
Now we are able to load the hyperdrive engine using the container and test it out.
$hyperdrive = $container->get('hyperdrive');
echo 'current fuel: ' . $hyperdrive->getFuel() . PHP_EOL; // 10000
echo 'traveling : ' . $hyperdrive->throttle(5) . PHP_EOL; // 2500
echo 'current fuel: ' . $hyperdrive->getFuel() . PHP_EOL; // 7500
Often we don't want to hardcode the constructor arguments, that's where parameters come in handy.
:hyperdrive.power: 500
:hyperdrive.fuel: 10000
@hyperdrive: Engine(:hyperdrive.power, :hyperdrive.fuel)
But we still can not refuel our engine without a mechanic. This brings us to the next example.
The second example class will be a Human with only one argument in the constructor which represents the name.
class Human
{
public $name;
public $job;
public function __construct(string $name) {
$this->name = $name;
}
public function setJob(string $job) {
$this->job = $job;
}
}
I'm a big Firefly fan so excuse all the references. Here comes our mechanic.
@kaylee: Human('Kaylee Frye')
- setJob('Mechanic')
Now we are also able to assign Kaylee as a mechanic to our engine.
@hyperdrive: Engine(:hyperdrive.power, :hyperdrive.fuel)
- setMechanic(@kaylee)
And voila we are able to refuel:
$hyperdrive = $container->get('hyperdrive');
$hyperdrive->getFuel();
$hyperdrive->throttle(5);
$hyperdrive->refuel(1000);
echo 'current fuel: ' . $hyperdrive->getFuel() . PHP_EOL; // 8500