Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Config/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace CodeIgniter\Settings\Config;

use CodeIgniter\Settings\Handlers\ArrayHandler;
use CodeIgniter\Settings\Handlers\DatabaseHandler;

class Settings
Expand All @@ -15,6 +16,14 @@ class Settings
*/
public $handlers = ['database'];

/**
* Array handler settings.
*/
public $array = [
'class' => ArrayHandler::class,
'writeable' => true,
];

/**
* Database handler settings.
*/
Expand Down
116 changes: 116 additions & 0 deletions src/Handlers/ArrayHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace CodeIgniter\Settings\Handlers;

/**
* Array Settings Handler
*
* Uses local storage to handle non-persistent
* Settings requests. Useful mostly for testing
* or extension by true persistent handlers.
*/
class ArrayHandler extends BaseHandler
{
/**
* Storage for general settings.
* Format: ['class' => ['property' => ['value', 'type']]]
*
* @var array<string,array<string,array>>
*/
private $general = [];

/**
* Storage for context settings.
* Format: ['context' => ['class' => ['property' => ['value', 'type']]]]
*
* @var array<string,array|null>
*/
private $contexts = [];

public function has(string $class, string $property, ?string $context = null): bool
{
return $this->hasStored($class, $property, $context);
}

public function get(string $class, string $property, ?string $context = null)
{
return $this->getStored($class, $property, $context);
}

public function set(string $class, string $property, $value = null, ?string $context = null)
{
$this->setStored($class, $property, $value, $context);
}

public function forget(string $class, string $property, ?string $context = null)
{
$this->forgetStored($class, $property, $context);
}

/**
* Checks whether this value is in storage.
*/
protected function hasStored(string $class, string $property, ?string $context): bool
{
if ($context === null) {
return isset($this->general[$class])
? array_key_exists($property, $this->general[$class])
: false;
}

return isset($this->contexts[$context][$class])
? array_key_exists($property, $this->contexts[$context][$class])
: false;
}

/**
* Retrieves a value from storage.
*
* @return mixed|null
*/
protected function getStored(string $class, string $property, ?string $context)
{
if (! $this->has($class, $property, $context)) {
return null;
}

return $context === null
? $this->parseValue(...$this->general[$class][$property])
: $this->parseValue(...$this->contexts[$context][$class][$property]);
}

/**
* Adds values to storage.
*
* @param mixed $value
*/
protected function setStored(string $class, string $property, $value, ?string $context): void
{
$type = gettype($value);
$value = $this->prepareValue($value);

if ($context === null) {
$this->general[$class][$property] = [
$value,
$type,
];
} else {
$this->contexts[$context][$class][$property] = [
$value,
$type,
];
}
}

/**
* Deletes an item from storage.
*/
protected function forgetStored(string $class, string $property, ?string $context): void
{
if ($context === null) {
unset($this->general[$class][$property]);
} else {
unset($this->contexts[$context][$class][$property]);
}
}
}
112 changes: 31 additions & 81 deletions src/Handlers/DatabaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
use RuntimeException;

/**
* Provides database storage for Settings.
* Uses local storage to minimize database calls.
* Provides database persistence for Settings.
* Uses ArrayHandler for storage to minimize database calls.
*/
class DatabaseHandler extends BaseHandler
class DatabaseHandler extends ArrayHandler
{
/**
* The database table to use.
Expand All @@ -19,20 +19,11 @@ class DatabaseHandler extends BaseHandler
private $table;

/**
* Storage for cached general settings.
* Format: ['class' => ['property' => ['value', 'type']]]
* Array of contexts that have been stored.
*
* @var array<string,array<string,array>>|null Will be null until hydrated
* @var ?string[]
*/
private $general;

/**
* Storage for cached context settings.
* Format: ['context' => ['class' => ['property' => ['value', 'type']]]]
*
* @var array<string,array|null>
*/
private $contexts = [];
private $hydrated = [];

/**
* Stores the configured database table.
Expand All @@ -49,15 +40,7 @@ public function has(string $class, string $property, ?string $context = null): b
{
$this->hydrate($context);

if ($context === null) {
return isset($this->general[$class])
? array_key_exists($property, $this->general[$class])
: false;
}

return isset($this->contexts[$context][$class])
? array_key_exists($property, $this->contexts[$context][$class])
: false;
return $this->hasStored($class, $property, $context);
}

/**
Expand All @@ -70,13 +53,7 @@ public function has(string $class, string $property, ?string $context = null): b
*/
public function get(string $class, string $property, ?string $context = null)
{
if (! $this->has($class, $property, $context)) {
return null;
}

return $context === null
? $this->parseValue(...$this->general[$class][$property])
: $this->parseValue(...$this->contexts[$context][$class][$property]);
return $this->getStored($class, $property, $context);
}

/**
Expand All @@ -90,9 +67,9 @@ public function get(string $class, string $property, ?string $context = null)
*/
public function set(string $class, string $property, $value = null, ?string $context = null)
{
$time = Time::now()->format('Y-m-d H:i:s');
$type = gettype($value);
$value = $this->prepareValue($value);
$time = Time::now()->format('Y-m-d H:i:s');
$type = gettype($value);
$prepared = $this->prepareValue($value);

// If it was stored then we need to update
if ($this->has($class, $property, $context)) {
Expand All @@ -101,7 +78,7 @@ public function set(string $class, string $property, $value = null, ?string $con
->where('key', $property)
->where('context', $context)
->update([
'value' => $value,
'value' => $prepared,
'type' => $type,
'context' => $context,
'updated_at' => $time,
Expand All @@ -112,31 +89,21 @@ public function set(string $class, string $property, $value = null, ?string $con
->insert([
'class' => $class,
'key' => $property,
'value' => $value,
'value' => $prepared,
'type' => $type,
'context' => $context,
'created_at' => $time,
'updated_at' => $time,
]);
}

// Update storage
if ($result === true) {
if ($context === null) {
$this->general[$class][$property] = [
$value,
$type,
];
} else {
$this->contexts[$context][$class][$property] = [
$value,
$type,
];
}
} else {
if ($result !== true) {
throw new RuntimeException(db_connect()->error()['message'] ?? 'Error writing to the database.');
}

// Update storage
$this->setStored($class, $property, $value, $context);

return $result;
}

Expand All @@ -160,64 +127,47 @@ public function forget(string $class, string $property, ?string $context = null)
}

// Delete from local storage
if ($context === null) {
unset($this->general[$class][$property]);
} else {
unset($this->contexts[$context][$class][$property]);
}
$this->forgetStored($class, $property, $context);

return $result;
}

/**
* Fetches values from the database in bulk to minimize calls.
* General is always fetched once, contexts are fetched in their
* entirety for each new request.
* General (null) is always fetched once, contexts are fetched
* in their entirety for each new request.
*
* @throws RuntimeException For database failures
*/
private function hydrate(?string $context)
{
// Check for completion
if (in_array($context, $this->hydrated, true)) {
return;
}

if ($context === null) {
// Check for completion
if ($this->general !== null) {
return;
}
$this->hydrated[] = null;

$this->general = [];
$query = db_connect()->table($this->table)->where('context', null);
$query = db_connect()->table($this->table)->where('context', null);
} else {
// Check for completion
if (isset($this->contexts[$context])) {
return;
}

$query = db_connect()->table($this->table)->where('context', $context);

// If general has not been hydrated we will do that at the same time
if ($this->general === null) {
$this->general = [];
if (! in_array(null, $this->hydrated, true)) {
$this->hydrated[] = null;
$query->orWhere('context', null);
}

$this->contexts[$context] = [];
$this->hydrated[] = $context;
}

if (is_bool($result = $query->get())) {
throw new RuntimeException(db_connect()->error()['message'] ?? 'Error reading from database.');
}

foreach ($result->getResultObject() as $row) {
$tuple = [
$row->value,
$row->type,
];

if ($row->context === null) {
$this->general[$row->class][$row->key] = $tuple;
} else {
$this->contexts[$row->context][$row->class][$row->key] = $tuple;
}
$this->setStored($row->class, $row->key, $this->parseValue($row->value, $row->type), $row->context);
}
}
}
Loading