Skip to content

Commit

Permalink
Refactored Setup colections with optional overwrite
Browse files Browse the repository at this point in the history
- Renamed Setup namespace & Collection classes to Builder
- Moved default Builder to main namespace
- Removed overwrite checking in default Builder
- Added optional overwrite checking to ValidatedBuilder
  • Loading branch information
shudd3r committed Nov 17, 2019
2 parents 6e11ec0 + 6febab0 commit 2d6ca66
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 82 deletions.
47 changes: 28 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ and MUST NOT be used as id prefix for stored `Record`._
Having container stored with `foo` identifier would make `foo.bar` record inaccessible.
The rules might be hard to follow with multiple entries and sub-containers, so runtime checks
were implemented. To instantiate `Setup` with integrity checks instantiate with static
constructor: `$setup = Setup::secure();` - more on that in following sections.
constructor: `$setup = Setup::validated();` - more on that in following sections.

#### Preconfigured setup - constructor parameters
`Setup` may be instantiated with [`Setup\Collection`](src/Setup/Collection.php) parameter that
may contain already configured records or sub-containers. This collection can be also created
with associative arrays passed to static constructor `Setup::withData()`. Both `Collection` and
`Setup` may be instantiated with implementation of [`Builder`](src/Builder.php) parameter that
may contain already configured records or sub-containers. This builder can be also created
with associative arrays passed to static constructor `Setup::withData()`. Both `Builder` and
`Setup::withData()` method takes associative `Recrod[]` and `ContainerInetrface[]` arrays.

```php
Expand All @@ -148,7 +148,7 @@ $containers = [
// ...
];

$setup = new Setup(new Setup\Collection($records, $containers));
$setup = new Setup(new Builder\ProtectedBuilder($records, $containers));
// or
$setup = Setup::withData($records, $containers);
```
Expand All @@ -159,22 +159,30 @@ work with invalid configuration anyway. In development environment those checks
because they allow to fail as early as possible and help localize the source of an error.

#### Secure setup & circular reference detection
Instantiating `Setup` with [`Setup\ValidatedCollection`](src/Setup/ValidatedCollection.php),
with `Setup::secure()` static constructor or passing `true` as third parameter of `Setup::withData()`
By default container setup doesn't check if given identifiers are already defined or whether
will cause name collision making some entries inaccessible (sub-containers with identifier used
record entry prefix).

Instantiating `Setup` with [`Builder\ValidatedBuilder`](src/Builder/ValidatedBuilder.php)
class, `Setup::validated()` static constructor or passing `true` as third parameter of `Setup::withData()`
will enable runtime integrity checks for container configuration and detect circular references
when resolving dependencies with recursive container calls. Container will be created with
identifiers that will be accessible, **call stack** will be added to all exceptions thrown from
container, and `ContainerInterface::get()` method will throw `CircularReferenceException` immediately
after subsequent call would try to retrieve currently resolved record without blowing up the stack.

> This feature comes with minor performance overhead, and checking this kind of developer errors have
almost no value in production. It is recommended to use it as **development tool** only.
If you want to use container setup overriding some default values, you need to disable overwrite checks
in validation mode. **Overwrite** might be allowed despite validation by instantiating `ValidatedBuilder`
with `$allowOverwrite` (third) parameter. Instantiating with `true`, passing it to `Setup::validated();`
or as forth parameter of `Setup::withData()` will have same effect.

> These checks are separated from default setup behavior, because they should not be required in production
environment. It is recommended to use them as **development tool** though.

Checking such errors just because you can is pointless, because there are much more config related
bugs that cannot be checked in other way than testing if application works before deploying it to
production (invalid values or identifiers in container calling methods). On the other hand if those
checks are causing visible drop in performance you probably using container to extensively (see
[recommended use](#recommended-use) section).
Integration tests are necessary in development, because misconfigured container will most likely crash
the application, and it cannot be controlled by code anyway. This way some performance overhead might be
eliminated from production, but if those checks are causing visible drop in performance you are probably
using container too extensively (see [recommended use](#recommended-use) section).

#### Composed entry - nested composition & decorator feature:
Entry called for existing record can reassign it with `compose()` method if it uses it
Expand All @@ -197,14 +205,15 @@ currently using it would fail on type-checking, and due to lazy instantiation co
can't ensure decorator use and possible errors will emerge at runtime.

#### Direct instantiation & container composition
All `Setup` does, beside ability to validate configuration, is providing help to compose final container
based on provided configuration and chosen options. The simple container with `Record[]` array would be
instantiated like this:
All `Setup` does, beside ability to validate configuration, is providing helper methods creating
Record entries and creating various container compositions based on given setup. Container can also
0be instantiated directly - for example simple container (with `Record[]` array) would be instantiated
this way:
```php
$container = new RecordContainer(new Records($records));
```
And here's an example composition of container with circular reference checking and encapsulated
sub-containers (`ContainerInterface[]` array):
When container needs circular reference checking and encapsulate some sub-containers it would need
to be instantiated (with `ContainerInterface[]` array) this way:
```php
$container = new CompositeContainer(new TrackedRecords($records), $containers);
```
Expand Down
26 changes: 9 additions & 17 deletions src/Setup/Collection.php → src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,13 @@
* with this source code in the file LICENSE.
*/

namespace Polymorphine\Container\Setup;
namespace Polymorphine\Container;

use Polymorphine\Container\Records;
use Polymorphine\Container\RecordContainer;
use Polymorphine\Container\CompositeContainer;
use Polymorphine\Container\Exception;
use Psr\Container\ContainerInterface;


class Collection
class Builder
{
protected const SEPARATOR = CompositeContainer::SEPARATOR;
protected const WRAP_PREFIX = 'WRAP>';

protected $records;
Expand All @@ -39,25 +34,17 @@ public function __construct(array $records = [], array $containers = [])
public function container(): ContainerInterface
{
return $this->containers
? new CompositeContainer(new Records($this->records), $this->containers)
: new RecordContainer(new Records($this->records));
? new CompositeContainer($this->records(), $this->containers)
: new RecordContainer($this->records());
}

public function addRecord(string $id, Records\Record $record): void
{
if (isset($this->records[$id])) {
throw Exception\InvalidIdException::alreadyDefined("`$id` record");
}

$this->records[$id] = $record;
}

public function addContainer(string $id, ContainerInterface $container): void
{
if (isset($this->containers[$id])) {
throw Exception\InvalidIdException::alreadyDefined("`$id` container");
}

$this->containers[$id] = $container;
}

Expand All @@ -74,6 +61,11 @@ public function wrapRecord(string $id): string
return $newId;
}

protected function records(): Records
{
return new Records($this->records);
}

private function wrappedId(string $id): string
{
$newId = static::WRAP_PREFIX . $id;
Expand Down
15 changes: 8 additions & 7 deletions src/Setup/Entry.php → src/Builder/Entry.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
* with this source code in the file LICENSE.
*/

namespace Polymorphine\Container\Setup;
namespace Polymorphine\Container\Builder;

use Polymorphine\Container\Builder;
use Polymorphine\Container\Records\Record;
use Polymorphine\Container\Exception;
use Psr\Container\ContainerInterface;
Expand All @@ -23,12 +24,12 @@
class Entry
{
private $name;
private $records;
private $builder;

public function __construct(string $name, Collection $records)
public function __construct(string $name, Builder $builder)
{
$this->name = $name;
$this->records = $records;
$this->builder = $builder;
}

/**
Expand All @@ -41,7 +42,7 @@ public function __construct(string $name, Collection $records)
*/
public function record(Record $record): void
{
$this->records->addRecord($this->name, $record);
$this->builder->addRecord($this->name, $record);
}

/**
Expand Down Expand Up @@ -95,7 +96,7 @@ public function compose(string $className, string ...$dependencies): void
{
$idx = array_search($this->name, $dependencies, true);
if ($idx !== false) {
$dependencies[$idx] = $this->records->wrapRecord($this->name);
$dependencies[$idx] = $this->builder->wrapRecord($this->name);
}

$this->record(new Record\ComposeRecord($className, ...$dependencies));
Expand Down Expand Up @@ -130,6 +131,6 @@ public function create(string $factoryId, string $method, string ...$arguments):
*/
public function container(ContainerInterface $container)
{
$this->records->addContainer($this->name, $container);
$this->builder->addContainer($this->name, $container);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,50 @@
* with this source code in the file LICENSE.
*/

namespace Polymorphine\Container\Setup;
namespace Polymorphine\Container\Builder;

use Polymorphine\Container\Builder;
use Polymorphine\Container\Records;
use Polymorphine\Container\RecordContainer;
use Polymorphine\Container\CompositeContainer;
use Polymorphine\Container\Exception;
use Polymorphine\Container\CompositeContainer;
use Psr\Container\ContainerInterface;


class ValidatedCollection extends Collection
class ValidatedBuilder extends Builder
{
private $allowOverwrite;
private $reservedIds = [];

public function __construct(array $records = [], array $containers = [])
public function __construct(array $records = [], array $containers = [], bool $allowOverwrite = false)
{
parent::__construct($records, $containers);
$this->allowOverwrite = $allowOverwrite;
$this->validateState();
}

public function container(): ContainerInterface
{
return $this->containers
? new CompositeContainer(new Records\TrackedRecords($this->records), $this->containers)
: new RecordContainer(new Records\TrackedRecords($this->records));
}

public function addRecord(string $id, Records\Record $record): void
{
$this->checkRecordId($id);
if (!$this->allowOverwrite && isset($this->records[$id])) {
throw Exception\InvalidIdException::alreadyDefined("`$id` record");
}
parent::addRecord($id, $record);
}

public function addContainer(string $id, ContainerInterface $container): void
{
$this->checkContainerId($id);
if (!$this->allowOverwrite && isset($this->containers[$id])) {
throw Exception\InvalidIdException::alreadyDefined("`$id` container");
}
parent::addContainer($id, $container);
}

protected function records(): Records
{
return new Records\TrackedRecords($this->records);
}

private function validateState()
{
array_map([$this, 'checkRecord'], array_keys($this->records), $this->records);
Expand All @@ -67,7 +73,7 @@ private function checkRecordId(string $id): void
throw Exception\InvalidIdException::alreadyDefined("`$id` container");
}

$separator = strpos($id, self::SEPARATOR);
$separator = strpos($id, CompositeContainer::SEPARATOR);
$reserved = $separator === false ? $id : substr($id, 0, $separator);
if (isset($this->containers[$reserved])) {
throw Exception\InvalidIdException::prefixConflict($reserved);
Expand All @@ -86,8 +92,8 @@ private function checkContainer(string $id, $value): void

private function checkContainerId(string $id): void
{
if (strpos($id, self::SEPARATOR) !== false) {
throw Exception\InvalidIdException::unexpectedPrefixSeparator(self::SEPARATOR, $id);
if (strpos($id, CompositeContainer::SEPARATOR) !== false) {
throw Exception\InvalidIdException::unexpectedPrefixSeparator(CompositeContainer::SEPARATOR, $id);
}

if (isset($this->reservedIds[$id])) {
Expand Down
47 changes: 30 additions & 17 deletions src/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

class Setup
{
private $collection;
private $builder;

public function __construct(Setup\Collection $collection = null)
public function __construct(Builder $builder = null)
{
$this->collection = $collection ?: new Setup\Collection();
$this->builder = $builder ?: new Builder();
}

/**
Expand All @@ -29,32 +29,45 @@ public function __construct(Setup\Collection $collection = null)
* Added entries will be validated for identifier conflicts and
* created container will be monitored for circular references.
*
* Additional $allowOverwrite parameter determines if adding entry with
* already defined id will be overwritten. Can be used to build container
* with container with default values that can change under some conditions.
*
* @param bool $allowOverwrite
*
* @return self
*/
public static function secure(): self
public static function validated(bool $allowOverwrite = false): self
{
return new self(new Setup\ValidatedCollection());
return new self(new Builder\ValidatedBuilder([], [], $allowOverwrite));
}

/**
* Creates Setup with predefined configuration.
*
* If `true` is passed as $validate param secure version of Setup
* will be created and predefined configuration will be validated.
* If `true` is passed as $validate param validated version of Setup
* will be created. Both passed data and added entries will be validated.
*
* @see Setup::secure()
* Additional $allowOverwrite parameter determines if adding entry with
* already defined id will be overwritten. Can be used to build container
* with container with default values that can change under some conditions.
*
* @param Records\Record[] $records
* @param ContainerInterface[] $containers
* @param bool $validate
* @param bool $allowOverwrite
*
* @return self
*/
public static function withData(array $records = [], array $containers = [], bool $validate = false): self
{
public static function withData(
array $records = [],
array $containers = [],
bool $validate = false,
bool $allowOverwrite = false
): self {
$collection = $validate
? new Setup\ValidatedCollection($records, $containers)
: new Setup\Collection($records, $containers);
? new Builder\ValidatedBuilder($records, $containers, $allowOverwrite)
: new Builder($records, $containers);
return new self($collection);
}

Expand All @@ -69,7 +82,7 @@ public static function withData(array $records = [], array $containers = [], boo
*/
public function container(): ContainerInterface
{
return $this->collection->container();
return $this->builder->container();
}

/**
Expand All @@ -78,11 +91,11 @@ public function container(): ContainerInterface
*
* @param string $name
*
* @return Setup\Entry
* @return Builder\Entry
*/
public function entry(string $name): Setup\Entry
public function entry(string $name): Builder\Entry
{
return new Setup\Entry($name, $this->collection);
return new Builder\Entry($name, $this->builder);
}

/**
Expand All @@ -95,7 +108,7 @@ public function entry(string $name): Setup\Entry
public function records(array $records): void
{
foreach ($records as $id => $record) {
$this->collection->addRecord($id, $record);
$this->builder->addRecord($id, $record);
}
}
}
Loading

0 comments on commit 2d6ca66

Please sign in to comment.