Skip to main content

Overview

GAC’s adapter system allows you to integrate with any database or caching solution. This guide shows you how to create custom implementations of the DatabaseAdapterInterface and CacheAdapterInterface.

Database Adapter Interface

To create a custom database adapter, implement the DatabaseAdapterInterface from src/Adapters/DatabaseAdapterInterface.php:5.

Required Methods

namespace DancasDev\GAC\Adapters;

interface DatabaseAdapterInterface {
    public function getRoles(string $entityType, string|int $entityId): array;
    public function getPermissions(string $entityType, string|int $entityId, array $roleIds = []): array;
    public function getRestrictions(string $entityType, string|int $entityId, array $roleIds = []): array;
    public function getModulesData(array $categoryIds = [], array $moduleIds = []): array;
    public function getEntitiesByRoles(array $roleIds): array;
}

Method Specifications

Retrieve roles assigned to an entity.Parameters:
  • $entityType - Entity type (‘1’ = user, ‘2’ = client)
  • $entityId - Entity identifier
Returns: Array of roles with structure:
[
    [
        'id' => 1,
        'code' => 'system_administrator',
        'priority' => 0
    ],
    [
        'id' => 2,
        'code' => 'manager',
        'priority' => 1
    ]
]
Example Implementation:
public function getRoles(string $entityType, string|int $entityId): array {
    $query = 'SELECT b.id, b.code, a.priority';
    $query .= ' FROM `gac_role_entity` AS a INNER JOIN `gac_role` AS b ON a.role_id = b.id';
    $query .= ' WHERE a.entity_type = :entity_type AND a.entity_id = :entity_id';
    $query .= ' AND a.is_disabled = \'0\' AND b.is_disabled = \'0\'';
    $query .= ' AND a.deleted_at IS NULL AND b.deleted_at IS NULL';
    $query .= ' ORDER BY a.priority ASC';
    
    $query = $this->connection->prepare($query);
    $query->bindParam(':entity_type', $entityType, PDO::PARAM_INT);
    $query->bindParam(':entity_id', $entityId, PDO::PARAM_INT);
    $query->execute();

    return $query->fetchAll(PDO::FETCH_ASSOC);
}
Retrieve permissions for an entity and its roles.Parameters:
  • $entityType - Entity type (‘1’ = user, ‘2’ = client)
  • $entityId - Entity identifier
  • $roleIds - Array of role IDs to include
Returns: Array of raw permission records:
[
    [
        'id' => 1,
        'from_entity_type' => '0',  // 0=role, 1=user, 2=client
        'from_entity_id' => 1,
        'to_entity_type' => '1',    // 0=category, 1=module
        'to_entity_id' => 3,
        'feature' => '0,1,2,3',     // Comma-separated features
        'level' => '1'              // 0=low, 1=normal, 2=high
    ]
]
Example Implementation:
public function getPermissions(string $entityType, string|int $entityId, array $roleIds = []): array {
    if($entityType === '0') {
        return [];
    }

    $query = 'SELECT id, from_entity_type, from_entity_id, to_entity_type, to_entity_id, feature, level';
    $query .= ' FROM `gac_module_access` WHERE ((`from_entity_type` = ? AND `from_entity_id` = ?)';
    foreach ($roleIds as $key => $id) {
        $query .= ' OR (`from_entity_type` = \'0\' AND `from_entity_id` = ?)';
    }

    $query .= ') AND `deleted_at` IS NULL AND `is_disabled` = \'0\'';
    $query .= ' ORDER BY `from_entity_type` DESC';
    
    $query = $this->connection->prepare($query);
    $query->execute(array_merge([$entityType, $entityId], $roleIds));

    return $query->fetchAll(PDO::FETCH_ASSOC);
}
Retrieve restrictions for an entity and its roles.Parameters:
  • $entityType - Entity type (‘1’ = user, ‘2’ = client, ‘3’ = global)
  • $entityId - Entity identifier
  • $roleIds - Array of role IDs to include
Returns: Array of restriction records:
[
    [
        'id' => 1,
        'entity_type' => '1',
        'entity_id' => 123,
        'category_code' => 'by_branch',
        'type_code' => 'allow',
        'data' => '{"l": ["5", "12"]}'
    ]
]
Example Implementation:
public function getRestrictions(string $entityType, string|int $entityId, array $roleIds = []): array {
    if($entityType === '0') {
        return [];
    }

    $query = 'SELECT a.id, a.entity_type, a.entity_id, c.code AS category_code, b.code AS type_code, a.data';
    $query .= ' FROM `gac_restriction` AS a';
    $query .= ' INNER JOIN `gac_restriction_method` AS b ON a.restriction_method_id = b.id';
    $query .= ' INNER JOIN `gac_restriction_category` AS c ON b.restriction_category_id = c.id';
    $query .= ' WHERE ((a.entity_type = ? AND a.entity_id = ?)';
    foreach ($roleIds as $key => $id) {
        $query .= ' OR (a.entity_type = \'0\' AND a.entity_id = ?)';
    }

    $query .= ') AND a.deleted_at IS NULL AND b.deleted_at IS NULL AND c.deleted_at IS NULL';
    $query .= ' AND a.is_disabled = \'0\' AND b.is_disabled = \'0\' AND c.is_disabled = \'0\'';
    $query .= ' ORDER BY a.entity_type DESC';
    
    $query = $this->connection->prepare($query);
    $query->execute(array_merge([$entityType, $entityId], $roleIds));

    return $query->fetchAll(PDO::FETCH_ASSOC);
}
Retrieve module data by category or module IDs.Parameters:
  • $categoryIds - Array of category IDs
  • $moduleIds - Array of module IDs
Returns: Array of module records:
[
    [
        'id' => 3,
        'module_category_id' => 1,
        'code' => 'users',
        'is_developing' => '0'
    ]
]
Example Implementation:
public function getModulesData(array $categoryIds = [], array $moduleIds = []): array {
    $hasModules = !empty($moduleIds);
    $hasCategories = !empty($categoryIds);
    if (!$hasModules && !$hasCategories) {
        return [];
    }

    $query = 'SELECT a.id, a.module_category_id, a.code, a.is_developing';
    $query .= ' FROM gac_module AS a INNER JOIN gac_module_category AS b ON a.module_category_id = b.id';
    $query .= ' WHERE (';
    if (!empty($categoryIds)) {
        $query .= 'a.module_category_id IN (' . implode(',', $categoryIds) . ')';
    }
    if ($hasModules) {
        $query .= ' OR a.id IN (' . implode(',', $moduleIds) . ')';
    }
    $query .= ') AND a.deleted_at IS NULL AND b.deleted_at IS NULL';
    $query .= ' AND a.is_disabled = \'0\' AND b.is_disabled = \'0\'';

    $query = $this->connection->prepare($query);
    $query->execute();

    return $query->fetchAll(PDO::FETCH_ASSOC);
}
Get all users and clients associated with specific roles.Parameters:
  • $roleIds - Array of role IDs
Returns: Array of entity records:
[
    [
        'id' => 1,
        'role_id' => 5,
        'entity_type' => '1',  // 1=user, 2=client
        'entity_id' => 123
    ]
]
Example Implementation:
function getEntitiesByRoles(array $roleIds): array {
    if (empty($roleIds)) {
        return [];
    }

    $query = 'SELECT id, role_id, entity_type, entity_id';
    $query .= ' FROM `gac_role_entity`';
    $query .= ' WHERE role_id IN (' . implode(',', $roleIds) . ')';
    $query .= ' AND is_disabled = \'0\' AND deleted_at IS NULL';
    
    $query = $this->connection->prepare($query);
    $query->execute();

    return $query->fetchAll(PDO::FETCH_ASSOC);
}

Custom Database Adapter Examples

PostgreSQL Adapter

use DancasDev\GAC\Adapters\DatabaseAdapterInterface;
use DancasDev\GAC\Exceptions\DatabaseAdapterException;

class PostgreSQLAdapter implements DatabaseAdapterInterface {
    private $connection;
    
    public function __construct(array $config) {
        try {
            $dsn = "pgsql:host={$config['host']};dbname={$config['database']}";
            $this->connection = new PDO($dsn, $config['username'], $config['password']);
            $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (\Throwable $th) {
            throw new DatabaseAdapterException('PostgreSQL connection failed: ' . $th->getMessage(), 0, $th);
        }
    }
    
    public function getRoles(string $entityType, string|int $entityId): array {
        $query = 'SELECT b.id, b.code, a.priority';
        $query .= ' FROM gac_role_entity AS a INNER JOIN gac_role AS b ON a.role_id = b.id';
        $query .= ' WHERE a.entity_type = $1 AND a.entity_id = $2';
        $query .= ' AND a.is_disabled = \'0\' AND b.is_disabled = \'0\'';
        $query .= ' AND a.deleted_at IS NULL AND b.deleted_at IS NULL';
        $query .= ' ORDER BY a.priority ASC';
        
        $stmt = $this->connection->prepare($query);
        $stmt->execute([$entityType, $entityId]);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    // Implement other methods...
}

MongoDB Adapter

use DancasDev\GAC\Adapters\DatabaseAdapterInterface;
use MongoDB\Client;

class MongoDBAdapter implements DatabaseAdapterInterface {
    private $db;
    
    public function __construct(array $config) {
        $client = new Client($config['uri']);
        $this->db = $client->{$config['database']};
    }
    
    public function getRoles(string $entityType, string|int $entityId): array {
        $pipeline = [
            [
                '$match' => [
                    'entity_type' => $entityType,
                    'entity_id' => (int)$entityId,
                    'is_disabled' => '0',
                    'deleted_at' => null
                ]
            ],
            [
                '$lookup' => [
                    'from' => 'gac_role',
                    'localField' => 'role_id',
                    'foreignField' => 'id',
                    'as' => 'role'
                ]
            ],
            ['$unwind' => '$role'],
            [
                '$match' => [
                    'role.is_disabled' => '0',
                    'role.deleted_at' => null
                ]
            ],
            [
                '$project' => [
                    '_id' => 0,
                    'id' => '$role.id',
                    'code' => '$role.code',
                    'priority' => 1
                ]
            ],
            ['$sort' => ['priority' => 1]]
        ];
        
        $result = $this->db->gac_role_entity->aggregate($pipeline);
        return $result->toArray();
    }
    
    // Implement other methods...
}

Doctrine ORM Adapter

use DancasDev\GAC\Adapters\DatabaseAdapterInterface;
use Doctrine\ORM\EntityManagerInterface;

class DoctrineAdapter implements DatabaseAdapterInterface {
    private EntityManagerInterface $em;
    
    public function __construct(EntityManagerInterface $em) {
        $this->em = $em;
    }
    
    public function getRoles(string $entityType, string|int $entityId): array {
        $qb = $this->em->createQueryBuilder();
        $qb->select('r.id', 'r.code', 're.priority')
            ->from('App\Entity\RoleEntity', 're')
            ->innerJoin('re.role', 'r')
            ->where('re.entityType = :entityType')
            ->andWhere('re.entityId = :entityId')
            ->andWhere('re.isDisabled = :disabled')
            ->andWhere('r.isDisabled = :disabled')
            ->andWhere('re.deletedAt IS NULL')
            ->andWhere('r.deletedAt IS NULL')
            ->setParameter('entityType', $entityType)
            ->setParameter('entityId', $entityId)
            ->setParameter('disabled', '0')
            ->orderBy('re.priority', 'ASC');
        
        return $qb->getQuery()->getArrayResult();
    }
    
    // Implement other methods...
}

Cache Adapter Interface

To create a custom cache adapter, implement the CacheAdapterInterface from src/Adapters/CacheAdapterInterface.php:5.

Required Methods

namespace DancasDev\GAC\Adapters;

interface CacheAdapterInterface {
    public function get(string $key): mixed;
    public function save(string $key, mixed $data, ?int $ttl = 60): bool;
    public function delete(string $key): bool;
    public function deleteMatching(string $pattern): int;
    public function clean(): bool;
}

Method Specifications

Retrieve cached value by key.Parameters:
  • $key - Cache key
Returns: Cached value (mixed) or null if not found/expired
public function get(string $key): mixed {
    $file = $this->cacheDir . DIRECTORY_SEPARATOR . $key;
    if (!file_exists($file)) {
        return null;
    }

    $data = json_decode(file_get_contents($file), true);
    
    if (empty($data) || (!empty($data['t']) && $data['t'] < time())) {
        $this->delete($key);
        return null;
    }

    return $data['v'] ?? null;
}
Store value in cache with TTL.Parameters:
  • $key - Cache key
  • $data - Data to cache (any type)
  • $ttl - Time to live in seconds (null = no expiration)
Returns: true on success, false on failure
public function save(string $key, $data, int|null $ttl = 60): bool {
    $file = $this->cacheDir . DIRECTORY_SEPARATOR . $key;
    $dataToSave = [
        't' => empty($ttl) ? null : time() + $ttl,
        'v' => $data
    ];

    return file_put_contents($file, json_encode($dataToSave)) !== false;
}
Delete specific cache key.Parameters:
  • $key - Cache key to delete
Returns: true on success, false if key doesn’t exist
public function delete(string $key): bool {
    $file = $this->cacheDir . DIRECTORY_SEPARATOR . $key;
    if (file_exists($file)) {
        return unlink($file);
    }
    return false;
}
Delete all keys matching a glob pattern.Parameters:
  • $pattern - Glob pattern (e.g., gac_p_*, myapp_*_123)
Returns: Number of deleted keys
public function deleteMatching(string $pattern): int {
    $deletedCount = 0;
    $files = glob($this->cacheDir . DIRECTORY_SEPARATOR . $pattern);
    foreach ($files as $file) {
        if (is_file($file) && unlink($file)) {
            $deletedCount++;
        }
    }
    return $deletedCount;
}
Clear all cache entries.Returns: true on success, false on failure
public function clean(): bool {
    $files = glob($this->cacheDir . DIRECTORY_SEPARATOR . '*');
    foreach ($files as $file) {
        if (is_file($file)) {
            unlink($file);
        }
    }
    return true;
}

Custom Cache Adapter Examples

Redis Adapter

use DancasDev\GAC\Adapters\CacheAdapterInterface;
use DancasDev\GAC\Exceptions\CacheAdapterException;

class RedisCacheAdapter implements CacheAdapterInterface {
    private $redis;
    private $prefix;
    
    public function __construct(array $config) {
        $this->redis = new \Redis();
        
        if (!$this->redis->connect($config['host'], $config['port'] ?? 6379)) {
            throw new CacheAdapterException('Redis connection failed');
        }
        
        if (isset($config['password'])) {
            $this->redis->auth($config['password']);
        }
        
        $this->redis->select($config['database'] ?? 0);
        $this->prefix = $config['prefix'] ?? 'gac:';
    }
    
    public function get(string $key): mixed {
        $data = $this->redis->get($this->prefix . $key);
        return $data ? json_decode($data, true) : null;
    }
    
    public function save(string $key, mixed $data, ?int $ttl = 60): bool {
        $encoded = json_encode($data);
        $fullKey = $this->prefix . $key;
        
        if ($ttl) {
            return $this->redis->setex($fullKey, $ttl, $encoded);
        }
        return $this->redis->set($fullKey, $encoded);
    }
    
    public function delete(string $key): bool {
        return $this->redis->del($this->prefix . $key) > 0;
    }
    
    public function deleteMatching(string $pattern): int {
        $keys = $this->redis->keys($this->prefix . $pattern);
        return !empty($keys) ? $this->redis->del(...$keys) : 0;
    }
    
    public function clean(): bool {
        return $this->redis->flushDB();
    }
}

// Usage
$redisAdapter = new RedisCacheAdapter([
    'host' => '127.0.0.1',
    'port' => 6379,
    'password' => 'your_password',
    'database' => 0,
    'prefix' => 'myapp:gac:'
]);

$gac->setCache('myapp', 3600, $redisAdapter);

Memcached Adapter

use DancasDev\GAC\Adapters\CacheAdapterInterface;
use DancasDev\GAC\Exceptions\CacheAdapterException;

class MemcachedAdapter implements CacheAdapterInterface {
    private $memcached;
    private $prefix;
    
    public function __construct(array $config) {
        $this->memcached = new \Memcached();
        $this->memcached->addServer(
            $config['host'] ?? '127.0.0.1',
            $config['port'] ?? 11211
        );
        
        $this->prefix = $config['prefix'] ?? 'gac_';
    }
    
    public function get(string $key): mixed {
        $data = $this->memcached->get($this->prefix . $key);
        return $this->memcached->getResultCode() === \Memcached::RES_SUCCESS ? $data : null;
    }
    
    public function save(string $key, mixed $data, ?int $ttl = 60): bool {
        return $this->memcached->set(
            $this->prefix . $key,
            $data,
            $ttl ?? 0
        );
    }
    
    public function delete(string $key): bool {
        return $this->memcached->delete($this->prefix . $key);
    }
    
    public function deleteMatching(string $pattern): int {
        // Memcached doesn't support pattern matching
        // You'd need to maintain a key registry
        return 0;
    }
    
    public function clean(): bool {
        return $this->memcached->flush();
    }
}

APCu Adapter

use DancasDev\GAC\Adapters\CacheAdapterInterface;

class APCuAdapter implements CacheAdapterInterface {
    private $prefix;
    
    public function __construct(string $prefix = 'gac_') {
        if (!extension_loaded('apcu')) {
            throw new \RuntimeException('APCu extension not loaded');
        }
        $this->prefix = $prefix;
    }
    
    public function get(string $key): mixed {
        $success = false;
        $data = apcu_fetch($this->prefix . $key, $success);
        return $success ? $data : null;
    }
    
    public function save(string $key, mixed $data, ?int $ttl = 60): bool {
        return apcu_store($this->prefix . $key, $data, $ttl ?? 0);
    }
    
    public function delete(string $key): bool {
        return apcu_delete($this->prefix . $key);
    }
    
    public function deleteMatching(string $pattern): int {
        $iterator = new \APCUIterator('/^' . preg_quote($this->prefix . $pattern, '/') . '/');
        $count = 0;
        foreach ($iterator as $entry) {
            if (apcu_delete($entry['key'])) {
                $count++;
            }
        }
        return $count;
    }
    
    public function clean(): bool {
        return apcu_clear_cache();
    }
}

Custom Restriction Classes

Extend the base Restriction class to create custom restriction types.

IP Address Restriction

use DancasDev\GAC\Restrictions\Restriction;

class ByIp extends Restriction {
    protected array $methods = [
        'allow' => 'allowIp',
        'deny' => 'denyIp'
    ];
    
    public function allowIp(array $internalData, array $externalData): bool {
        // Validate data integrity
        if (!$this->validateDataIntegrity($internalData, ['ips' => ['array']])) {
            return false;
        }
        if (!$this->validateDataIntegrity($externalData, ['ip' => ['string']])) {
            return false;
        }
        
        $userIp = $externalData['ip'];
        foreach ($internalData['ips'] as $allowedIp) {
            if ($this->ipMatches($userIp, $allowedIp)) {
                return true;
            }
        }
        
        return false;
    }
    
    public function denyIp(array $internalData, array $externalData): bool {
        if (!$this->validateDataIntegrity($internalData, ['ips' => ['array']])) {
            return false;
        }
        if (!$this->validateDataIntegrity($externalData, ['ip' => ['string']])) {
            return false;
        }
        
        $userIp = $externalData['ip'];
        foreach ($internalData['ips'] as $deniedIp) {
            if ($this->ipMatches($userIp, $deniedIp)) {
                return false;
            }
        }
        
        return true;
    }
    
    private function ipMatches(string $ip, string $range): bool {
        // Support CIDR notation
        if (strpos($range, '/') !== false) {
            list($subnet, $mask) = explode('/', $range);
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);
            $mask_long = -1 << (32 - (int)$mask);
            return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
        }
        
        // Exact match
        return $ip === $range;
    }
}

// Register the restriction
use DancasDev\GAC\Restrictions\Restrictions;

Restrictions::register('by_ip', ByIp::class);

// Database setup
/*
INSERT INTO gac_restriction_category (name, code, created_at)
VALUES ('By IP Address', 'by_ip', UNIX_TIMESTAMP());

INSERT INTO gac_restriction_method (restriction_category_id, name, code, created_at)
VALUES (last_insert_id(), 'Allow IPs', 'allow', UNIX_TIMESTAMP());

INSERT INTO gac_restriction (entity_type, entity_id, restriction_method_id, data, created_at)
VALUES ('1', 123, last_insert_id(), '{"ips": ["192.168.1.0/24", "10.0.0.1"]}', UNIX_TIMESTAMP());
*/

// Usage
$restrictions = $gac->getRestrictions();
$ipRestriction = $restrictions->get('by_ip');

if ($ipRestriction) {
    $isAllowed = $ipRestriction->run(['ip' => $_SERVER['REMOTE_ADDR']]);
    if (!$isAllowed) {
        die('Your IP address is not allowed.');
    }
}

Geolocation Restriction

use DancasDev\GAC\Restrictions\Restriction;

class ByCountry extends Restriction {
    protected array $methods = [
        'allow' => 'allowCountries',
        'deny' => 'denyCountries'
    ];
    
    public function allowCountries(array $internalData, array $externalData): bool {
        if (!$this->validateDataIntegrity($internalData, ['countries' => ['array']])) {
            return false;
        }
        if (!$this->validateDataIntegrity($externalData, ['country' => ['string']])) {
            return false;
        }
        
        return in_array($externalData['country'], $internalData['countries']);
    }
    
    public function denyCountries(array $internalData, array $externalData): bool {
        if (!$this->validateDataIntegrity($internalData, ['countries' => ['array']])) {
            return false;
        }
        if (!$this->validateDataIntegrity($externalData, ['country' => ['string']])) {
            return false;
        }
        
        return !in_array($externalData['country'], $internalData['countries']);
    }
}

// Register
Restrictions::register('by_country', ByCountry::class);

// Usage with GeoIP
$geoip = geoip_record_by_name($_SERVER['REMOTE_ADDR']);
$country = $geoip['country_code'] ?? 'XX';

$restrictions = $gac->getRestrictions();
$countryRestriction = $restrictions->get('by_country');

if ($countryRestriction && !$countryRestriction->run(['country' => $country])) {
    die('Service not available in your country.');
}

Using Custom Adapters

Complete Example

use DancasDev\GAC\GAC;
use App\Adapters\PostgreSQLAdapter;
use App\Adapters\RedisCacheAdapter;
use App\Restrictions\ByIp;
use DancasDev\GAC\Restrictions\Restrictions;

// Initialize custom adapters
$dbAdapter = new PostgreSQLAdapter([
    'host' => 'localhost',
    'database' => 'myapp',
    'username' => 'postgres',
    'password' => 'password'
]);

$cacheAdapter = new RedisCacheAdapter([
    'host' => '127.0.0.1',
    'port' => 6379,
    'database' => 0,
    'prefix' => 'myapp:gac:'
]);

// Register custom restrictions
Restrictions::register('by_ip', ByIp::class);

// Setup GAC
$gac = new GAC();
$gac->setDatabase($dbAdapter);
$gac->setCache('myapp', 3600, $cacheAdapter);
$gac->setEntity('user', $userId);

// Use GAC normally
$permissions = $gac->getPermissions();
$restrictions = $gac->getRestrictions();

// Check IP restriction
$ipRestriction = $restrictions->get('by_ip');
if ($ipRestriction && !$ipRestriction->run(['ip' => $_SERVER['REMOTE_ADDR']])) {
    http_response_code(403);
    die('Access denied from your IP address.');
}

Testing Custom Adapters

Database Adapter Test

use PHPUnit\Framework\TestCase;

class PostgreSQLAdapterTest extends TestCase {
    private $adapter;
    
    protected function setUp(): void {
        $this->adapter = new PostgreSQLAdapter([
            'host' => 'localhost',
            'database' => 'test_db',
            'username' => 'test',
            'password' => 'test'
        ]);
    }
    
    public function testGetRoles() {
        $roles = $this->adapter->getRoles('1', 123);
        
        $this->assertIsArray($roles);
        $this->assertNotEmpty($roles);
        $this->assertArrayHasKey('id', $roles[0]);
        $this->assertArrayHasKey('code', $roles[0]);
        $this->assertArrayHasKey('priority', $roles[0]);
    }
    
    public function testGetPermissions() {
        $permissions = $this->adapter->getPermissions('1', 123, [1, 2]);
        
        $this->assertIsArray($permissions);
        foreach ($permissions as $perm) {
            $this->assertArrayHasKey('id', $perm);
            $this->assertArrayHasKey('feature', $perm);
            $this->assertArrayHasKey('level', $perm);
        }
    }
}

Cache Adapter Test

class RedisCacheAdapterTest extends TestCase {
    private $adapter;
    
    protected function setUp(): void {
        $this->adapter = new RedisCacheAdapter([
            'host' => '127.0.0.1',
            'port' => 6379,
            'database' => 15  // Use separate test database
        ]);
        $this->adapter->clean();
    }
    
    public function testSaveAndGet() {
        $data = ['test' => 'value'];
        $this->assertTrue($this->adapter->save('test_key', $data, 60));
        $this->assertEquals($data, $this->adapter->get('test_key'));
    }
    
    public function testDelete() {
        $this->adapter->save('test_key', ['data'], 60);
        $this->assertTrue($this->adapter->delete('test_key'));
        $this->assertNull($this->adapter->get('test_key'));
    }
    
    public function testDeleteMatching() {
        $this->adapter->save('gac_p_1', ['data1'], 60);
        $this->adapter->save('gac_p_2', ['data2'], 60);
        $this->adapter->save('gac_r_1', ['data3'], 60);
        
        $deleted = $this->adapter->deleteMatching('gac_p_*');
        $this->assertEquals(2, $deleted);
        $this->assertNull($this->adapter->get('gac_p_1'));
        $this->assertNull($this->adapter->get('gac_p_2'));
        $this->assertNotNull($this->adapter->get('gac_r_1'));
    }
}

Best Practices

Always throw appropriate exceptions:
use DancasDev\GAC\Exceptions\DatabaseAdapterException;
use DancasDev\GAC\Exceptions\CacheAdapterException;

// In database adapter
try {
    $result = $this->connection->query($sql);
} catch (\Throwable $e) {
    throw new DatabaseAdapterException(
        'Query failed: ' . $e->getMessage(),
        0,
        $e
    );
}

// In cache adapter
if (!$this->redis->connect($host, $port)) {
    throw new CacheAdapterException('Redis connection failed');
}
Reuse connections when possible:
class PostgreSQLAdapter implements DatabaseAdapterInterface {
    private static $sharedConnection;
    
    public function __construct(array $config) {
        if (self::$sharedConnection === null) {
            self::$sharedConnection = new PDO(...);
        }
        $this->connection = self::$sharedConnection;
    }
}
Don’t connect until needed:
class RedisCacheAdapter implements CacheAdapterInterface {
    private $redis;
    private $config;
    private $connected = false;
    
    public function __construct(array $config) {
        $this->config = $config;
    }
    
    private function connect() {
        if (!$this->connected) {
            $this->redis = new \Redis();
            $this->redis->connect($this->config['host'], $this->config['port']);
            $this->connected = true;
        }
    }
    
    public function get(string $key): mixed {
        $this->connect();
        return $this->redis->get($key);
    }
}
Use strict types and return type declarations:
declare(strict_types=1);

class MyAdapter implements DatabaseAdapterInterface {
    public function getRoles(string $entityType, string|int $entityId): array {
        // Implementation
    }
}

Next Steps

Setup Database

Review database schema and customization

Cache Management

Learn advanced caching strategies

Build docs developers (and LLMs) love