Skip to content

Commit

Permalink
Condition interface, abstract and decorators.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmatosbc committed Dec 23, 2024
1 parent 3ddda5a commit b3de475
Show file tree
Hide file tree
Showing 14 changed files with 762 additions and 19 deletions.
126 changes: 125 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Ananke

[![PHP Lint](https://github.com/cmatosbc/ananke/actions/workflows/lint.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/lint.yml) [![PHPUnit Tests](https://github.com/cmatosbc/ananke/actions/workflows/phpunit.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/phpunit.yml) [![PHP Composer](https://github.com/cmatosbc/ananke/actions/workflows/composer.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/composer.yml)
[![PHP Lint](https://github.com/cmatosbc/ananke/actions/workflows/lint.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/lint.yml) [![PHPUnit Tests](https://github.com/cmatosbc/ananke/actions/workflows/phpunit.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/phpunit.yml) [![PHP Composer](https://github.com/cmatosbc/ananke/actions/workflows/composer.yml/badge.svg)](https://github.com/cmatosbc/ananke/actions/workflows/composer.yml) [![Latest Stable Version](http://poser.pugx.org/cmatosbc/ananke/v)](https://packagist.org/packages/cmatosbc/ananke) [![License](http://poser.pugx.org/cmatosbc/ananke/license)](https://packagist.org/packages/cmatosbc/ananke)

A flexible PHP service container that supports conditional service instantiation. This package allows you to register services with multiple conditions that must be met before the service can be instantiated.

## Requirements

- PHP 8.0 or higher

## Features

- Register services with their class names and constructor parameters
Expand Down Expand Up @@ -66,6 +70,126 @@ if ($factory->has('premium.feature')) {
}
```

## Condition Decorators

Ananke provides a powerful set of condition decorators that allow you to compose complex condition logic:

### Not Condition

Negate any condition:

```php
use Ananke\Conditions\{NotCondition, CallableCondition};

// Basic condition
$factory->registerCondition('is-maintenance',
new CallableCondition('is-maintenance', fn() => $maintenance->isActive()));

// Negate it
$factory->registerCondition('not-maintenance',
new NotCondition($factory->getCondition('is-maintenance')));

// Use in service
$factory->register('api', APIService::class);
$factory->associateCondition('api', 'not-maintenance');
```

### Cached Condition

Cache expensive condition evaluations:

```php
use Ananke\Conditions\CachedCondition;

// Cache an expensive API check for 1 hour
$factory->registerCondition('api-status',
new CachedCondition(
new CallableCondition('api-check', fn() => $api->checkStatus()),
3600 // Cache for 1 hour
));
```

### AND/OR Conditions

Combine multiple conditions with logical operators:

```php
use Ananke\Conditions\{AndCondition, OrCondition};

// Premium access: User must be premium OR have a trial subscription
$factory->registerCondition('can-access-premium',
new OrCondition([
new CallableCondition('is-premium', fn() => $user->isPremium()),
new CallableCondition('has-trial', fn() => $user->hasTrial())
]));

// Database write: Need both connection AND proper permissions
$factory->registerCondition('can-write-db',
new AndCondition([
new CallableCondition('is-connected', fn() => $db->isConnected()),
new CallableCondition('has-permissions', fn() => $user->canWrite())
]));
```

### Complex Condition Compositions

Combine decorators for complex logic:

```php
// ((isPremium OR hasTrial) AND notMaintenance) AND (hasQuota OR isUnlimited)
$factory->registerCondition('can-use-service',
new AndCondition([
// Premium access check
new OrCondition([
new CallableCondition('premium', fn() => $user->isPremium()),
new CallableCondition('trial', fn() => $user->hasTrial())
]),
// Not in maintenance
new NotCondition(
new CallableCondition('maintenance', fn() => $maintenance->isActive())
),
// Resource availability
new OrCondition([
new CallableCondition('has-quota', fn() => $user->hasQuota()),
new CallableCondition('unlimited', fn() => $user->isUnlimited())
])
])
);

// Cache the entire complex condition
$factory->registerCondition('cached-access-check',
new CachedCondition(
$factory->getCondition('can-use-service'),
300 // Cache for 5 minutes
)
);
```

### Best Practices

1. **Caching**: Use `CachedCondition` for:
- External API calls
- Database queries
- File system checks
- Any expensive operations

2. **Composition**: Build complex conditions gradually:
- Start with simple conditions
- Combine them using AND/OR
- Add negation where needed
- Cache at appropriate levels

3. **Naming**: Use clear, descriptive names:
- Negated: prefix with 'not-'
- Cached: prefix with 'cached-'
- Combined: use descriptive action names

4. **Testing**: Test complex conditions thoroughly:
- Verify each sub-condition
- Test boundary cases
- Ensure proper short-circuit evaluation
- Validate cache behavior

## Real-World Use Cases

### 1. Environment-Specific Services
Expand Down
20 changes: 18 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@
"email": "[email protected]"
}
],
"minimum-stability": "dev",
"require": {
"php": ">=8.0"
},
"require-dev": {
"phpunit/phpunit": "11.5.x-dev"
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.0"
},
"autoload": {
"psr-4": {
"Ananke\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Ananke\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"cs-check": "phpcs",
"cs-fix": "phpcbf"
},
"config": {
"sort-packages": true
}
}
24 changes: 24 additions & 0 deletions src/Conditions/AbstractCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Ananke\Conditions;

/**
* Base class for conditions providing common functionality.
*/
abstract class AbstractCondition implements ConditionInterface
{
protected string $name;

/**
* @param string $name Unique identifier for this condition
*/
public function __construct(string $name)
{
$this->name = $name;
}

public function getName(): string
{
return $this->name;
}
}
32 changes: 32 additions & 0 deletions src/Conditions/AndCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Ananke\Conditions;

/**
* Decorator that combines multiple conditions with AND logic.
*/
class AndCondition extends AbstractCondition
{
/** @var ConditionInterface[] */
private array $conditions;

/**
* @param ConditionInterface[] $conditions List of conditions to combine
*/
public function __construct(array $conditions)
{
$names = array_map(fn($c) => $c->getName(), $conditions);
parent::__construct('and_' . implode('_', $names));
$this->conditions = $conditions;
}

public function evaluate(): bool
{
foreach ($this->conditions as $condition) {
if (!$condition->evaluate()) {
return false;
}
}
return true;
}
}
53 changes: 53 additions & 0 deletions src/Conditions/CachedCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Ananke\Conditions;

/**
* Decorator that caches another condition's result for a specified duration.
*/
class CachedCondition extends AbstractCondition
{
private ConditionInterface $condition;
private ?bool $cachedResult = null;
private ?int $cachedAt = null;
private int $ttl;

/**
* @param ConditionInterface $condition The condition to cache
* @param int $ttl Time to live in seconds
*/
public function __construct(ConditionInterface $condition, int $ttl)
{
parent::__construct("cached_{$condition->getName()}");
$this->condition = $condition;
$this->ttl = $ttl;
}

public function evaluate(): bool
{
$now = time();

// If we have a cached result and it hasn't expired
if ($this->cachedResult !== null &&
$this->cachedAt !== null &&
$now - $this->cachedAt < $this->ttl
) {
return $this->cachedResult;
}

// Evaluate and cache the result
$this->cachedResult = $this->condition->evaluate();
$this->cachedAt = $now;

return $this->cachedResult;
}

/**
* Clear the cached result.
*/
public function clearCache(): void
{
$this->cachedResult = null;
$this->cachedAt = null;
}
}
32 changes: 32 additions & 0 deletions src/Conditions/CallableCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Ananke\Conditions;

/**
* Basic condition implementation that wraps a callable.
*/
class CallableCondition implements ConditionInterface
{
private string $name;
private \Closure $validator;

/**
* @param string $name Unique identifier for this condition
* @param callable $validator Function that returns bool when evaluated
*/
public function __construct(string $name, callable $validator)
{
$this->name = $name;
$this->validator = \Closure::fromCallable($validator);
}

public function evaluate(): bool
{
return ($this->validator)();
}

public function getName(): string
{
return $this->name;
}
}
26 changes: 26 additions & 0 deletions src/Conditions/ConditionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Ananke\Conditions;

/**
* Interface for service conditions.
*
* Conditions are used to determine whether a service can be instantiated.
* They can be simple callables or complex compositions using decorators.
*/
interface ConditionInterface
{
/**
* Evaluate the condition.
*
* @return bool True if the condition is met, false otherwise
*/
public function evaluate(): bool;

/**
* Get a unique identifier for this condition.
*
* @return string Condition identifier
*/
public function getName(): string;
}
25 changes: 25 additions & 0 deletions src/Conditions/NotCondition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Ananke\Conditions;

/**
* Decorator that negates another condition's result.
*/
class NotCondition extends AbstractCondition
{
private ConditionInterface $condition;

/**
* @param ConditionInterface $condition The condition to negate
*/
public function __construct(ConditionInterface $condition)
{
parent::__construct("not_{$condition->getName()}");
$this->condition = $condition;
}

public function evaluate(): bool
{
return !$this->condition->evaluate();
}
}
Loading

0 comments on commit b3de475

Please sign in to comment.