Initial project files: BESCMS full source
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache;
|
||||
|
||||
use CodeIgniter\Cache\Exceptions\CacheException;
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\Test\Mock\MockCache;
|
||||
use Config\Cache;
|
||||
|
||||
/**
|
||||
* A factory for loading the desired
|
||||
*
|
||||
* @see \CodeIgniter\Cache\CacheFactoryTest
|
||||
*/
|
||||
class CacheFactory
|
||||
{
|
||||
/**
|
||||
* The class to use when mocking
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $mockClass = MockCache::class;
|
||||
|
||||
/**
|
||||
* The service to inject the mock as
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $mockServiceName = 'cache';
|
||||
|
||||
/**
|
||||
* Attempts to create the desired cache handler, based upon the
|
||||
*
|
||||
* @param non-empty-string|null $handler
|
||||
* @param non-empty-string|null $backup
|
||||
*
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public static function getHandler(Cache $config, ?string $handler = null, ?string $backup = null)
|
||||
{
|
||||
if (! isset($config->validHandlers) || $config->validHandlers === []) {
|
||||
throw CacheException::forInvalidHandlers();
|
||||
}
|
||||
|
||||
if (! isset($config->handler) || ! isset($config->backupHandler)) {
|
||||
throw CacheException::forNoBackup();
|
||||
}
|
||||
|
||||
$handler ??= $config->handler;
|
||||
$backup ??= $config->backupHandler;
|
||||
|
||||
if (! array_key_exists($handler, $config->validHandlers) || ! array_key_exists($backup, $config->validHandlers)) {
|
||||
throw CacheException::forHandlerNotFound();
|
||||
}
|
||||
|
||||
$adapter = new $config->validHandlers[$handler]($config);
|
||||
|
||||
if (! $adapter->isSupported()) {
|
||||
$adapter = new $config->validHandlers[$backup]($config);
|
||||
|
||||
if (! $adapter->isSupported()) {
|
||||
// Fall back to the dummy adapter.
|
||||
$adapter = new $config->validHandlers['dummy']();
|
||||
}
|
||||
}
|
||||
|
||||
// If $adapter->initialize() throws a CriticalError exception, we will attempt to
|
||||
// use the $backup handler, if that also fails, we resort to the dummy handler.
|
||||
try {
|
||||
$adapter->initialize();
|
||||
} catch (CriticalError $e) {
|
||||
log_message('critical', $e . ' Resorting to using ' . $backup . ' handler.');
|
||||
|
||||
// get the next best cache handler (or dummy if the $backup also fails)
|
||||
$adapter = self::getHandler($config, $backup, 'dummy');
|
||||
}
|
||||
|
||||
return $adapter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache;
|
||||
|
||||
interface CacheInterface
|
||||
{
|
||||
/**
|
||||
* Takes care of any handler-specific setup that must be done.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize();
|
||||
|
||||
/**
|
||||
* Attempts to fetch an item from the cache store.
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key);
|
||||
|
||||
/**
|
||||
* Saves an item to the cache store.
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
* @param mixed $value The data to save
|
||||
* @param int $ttl Time To Live, in seconds (default 60)
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60);
|
||||
|
||||
/**
|
||||
* Deletes a specific item from the cache store.
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public function delete(string $key);
|
||||
|
||||
/**
|
||||
* Performs atomic incrementation of a raw stored value.
|
||||
*
|
||||
* @param string $key Cache ID
|
||||
* @param int $offset Step/value to increase by
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1);
|
||||
|
||||
/**
|
||||
* Performs atomic decrementation of a raw stored value.
|
||||
*
|
||||
* @param string $key Cache ID
|
||||
* @param int $offset Step/value to increase by
|
||||
*
|
||||
* @return bool|int
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1);
|
||||
|
||||
/**
|
||||
* Will delete all items in the entire cache.
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public function clean();
|
||||
|
||||
/**
|
||||
* Returns information on the entire cache.
|
||||
*
|
||||
* The information returned and the structure of the data
|
||||
* varies depending on the handler.
|
||||
*
|
||||
* @return array<array-key, mixed>|false|object|null
|
||||
*/
|
||||
public function getCacheInfo();
|
||||
|
||||
/**
|
||||
* Returns detailed information about the specific item in the cache.
|
||||
*
|
||||
* @param string $key Cache item name.
|
||||
*
|
||||
* @return array<string, mixed>|false|null Returns null if the item does not exist, otherwise array<string, mixed>
|
||||
* with at least the 'expire' key for absolute epoch expiry (or null).
|
||||
* Some handlers may return false when an item does not exist, which is deprecated.
|
||||
*/
|
||||
public function getMetaData(string $key);
|
||||
|
||||
/**
|
||||
* Determines if the driver is supported on this system.
|
||||
*/
|
||||
public function isSupported(): bool;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\DebugTraceableTrait;
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
|
||||
class CacheException extends RuntimeException
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
/**
|
||||
* Thrown when handler has no permission to write cache.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forUnableToWrite(string $path)
|
||||
{
|
||||
return new static(lang('Cache.unableToWrite', [$path]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an unrecognized handler is used.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidHandlers()
|
||||
{
|
||||
return new static(lang('Cache.invalidHandlers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when no backup handler is setup in config.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forNoBackup()
|
||||
{
|
||||
return new static(lang('Cache.noBackup'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when specified handler was not found.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function forHandlerNotFound()
|
||||
{
|
||||
return new static(lang('Cache.handlerNotFound'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache;
|
||||
|
||||
use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler;
|
||||
use CodeIgniter\Config\Factories;
|
||||
|
||||
final class FactoriesCache
|
||||
{
|
||||
private readonly CacheInterface|FileVarExportHandler $cache;
|
||||
|
||||
public function __construct(CacheInterface|FileVarExportHandler|null $cache = null)
|
||||
{
|
||||
$this->cache = $cache ?? new FileVarExportHandler();
|
||||
}
|
||||
|
||||
public function save(string $component): void
|
||||
{
|
||||
if (! Factories::isUpdated($component)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Factories::getComponentInstances($component);
|
||||
|
||||
$this->cache->save($this->getCacheKey($component), $data, 3600 * 24);
|
||||
}
|
||||
|
||||
private function getCacheKey(string $component): string
|
||||
{
|
||||
return 'FactoriesCache_' . $component;
|
||||
}
|
||||
|
||||
public function load(string $component): bool
|
||||
{
|
||||
$key = $this->getCacheKey($component);
|
||||
|
||||
$data = $this->cache->get($key);
|
||||
|
||||
if (! is_array($data) || $data === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Factories::setComponentInstances($component, $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(string $component): void
|
||||
{
|
||||
$this->cache->delete($this->getCacheKey($component));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\FactoriesCache;
|
||||
|
||||
final class FileVarExportHandler
|
||||
{
|
||||
private string $path = WRITEPATH . 'cache';
|
||||
|
||||
public function save(string $key, mixed $val): void
|
||||
{
|
||||
$val = var_export($val, true);
|
||||
|
||||
// Write to temp file first to ensure atomicity
|
||||
$tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp';
|
||||
file_put_contents($tmp, '<?php return ' . $val . ';', LOCK_EX);
|
||||
|
||||
rename($tmp, $this->path . "/{$key}");
|
||||
}
|
||||
|
||||
public function delete(string $key): void
|
||||
{
|
||||
@unlink($this->path . "/{$key}");
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
return @include $this->path . "/{$key}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use Config\Cache;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Base class for cache handling
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\BaseHandlerTest
|
||||
*/
|
||||
abstract class BaseHandler implements CacheInterface
|
||||
{
|
||||
/**
|
||||
* Reserved characters that cannot be used in a key or tag. May be overridden by the config.
|
||||
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
|
||||
*
|
||||
* @deprecated in favor of the Cache config
|
||||
*/
|
||||
public const RESERVED_CHARACTERS = '{}()/\@:';
|
||||
|
||||
/**
|
||||
* Maximum key length.
|
||||
*/
|
||||
public const MAX_KEY_LENGTH = PHP_INT_MAX;
|
||||
|
||||
/**
|
||||
* Prefix to apply to cache keys.
|
||||
* May not be used by all handlers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* Validates a cache key according to PSR-6.
|
||||
* Keys that exceed MAX_KEY_LENGTH are hashed.
|
||||
* From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158
|
||||
*
|
||||
* @param mixed $key The key to validate
|
||||
* @param string $prefix Optional prefix to include in length calculations
|
||||
*
|
||||
* @throws InvalidArgumentException When $key is not valid
|
||||
*/
|
||||
public static function validateKey($key, $prefix = ''): string
|
||||
{
|
||||
if (! is_string($key)) {
|
||||
throw new InvalidArgumentException('Cache key must be a string');
|
||||
}
|
||||
if ($key === '') {
|
||||
throw new InvalidArgumentException('Cache key cannot be empty.');
|
||||
}
|
||||
|
||||
$reserved = config(Cache::class)->reservedCharacters;
|
||||
|
||||
if ($reserved !== '' && strpbrk($key, $reserved) !== false) {
|
||||
throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved);
|
||||
}
|
||||
|
||||
// If the key with prefix exceeds the length then return the hashed version
|
||||
return strlen($prefix . $key) > static::MAX_KEY_LENGTH ? $prefix . md5($key) : $prefix . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the cache, or execute the given Closure and store the result.
|
||||
*
|
||||
* @param string $key Cache item name
|
||||
* @param int $ttl Time to live
|
||||
* @param Closure(): mixed $callback Callback return value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function remember(string $key, int $ttl, Closure $callback)
|
||||
{
|
||||
$value = $this->get($key);
|
||||
|
||||
if ($value !== null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$this->save($key, $value = $callback(), $ttl);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes items from the cache store matching a given pattern.
|
||||
*
|
||||
* @param string $pattern Cache items glob-style pattern
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
throw new BadMethodCallException('The deleteMatching method is not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Dummy cache handler
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\DummyHandlerTest
|
||||
*/
|
||||
class DummyHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function remember(string $key, int $ttl, Closure $callback)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use CodeIgniter\Cache\Exceptions\CacheException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Cache;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* File system cache handler
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\FileHandlerTest
|
||||
*/
|
||||
class FileHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* Maximum key length.
|
||||
*/
|
||||
public const MAX_KEY_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Where to store cached files on the disk.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Mode for the stored files.
|
||||
* Must be chmod-safe (octal).
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @see https://www.php.net/manual/en/function.chmod.php
|
||||
*/
|
||||
protected $mode;
|
||||
|
||||
/**
|
||||
* Note: Use `CacheFactory::getHandler()` to instantiate.
|
||||
*
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function __construct(Cache $config)
|
||||
{
|
||||
/*
|
||||
$options = [
|
||||
...['storePath' => WRITEPATH . 'cache', 'mode' => 0640],
|
||||
...$config->file,
|
||||
];*/
|
||||
$options = array_merge(
|
||||
['storePath' => WRITEPATH . 'cache', 'mode' => 0640],
|
||||
$config->file ?? []
|
||||
);
|
||||
|
||||
$this->path = $options['storePath'] !== '' ? $options['storePath'] : WRITEPATH . 'cache';
|
||||
$this->path = rtrim($this->path, '\\/') . '/';
|
||||
|
||||
if (! is_really_writable($this->path)) {
|
||||
throw CacheException::forUnableToWrite($this->path);
|
||||
}
|
||||
|
||||
$this->mode = $options['mode'];
|
||||
$this->prefix = $config->prefix;
|
||||
|
||||
helper('filesystem');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
$data = $this->getItem($key);
|
||||
|
||||
return is_array($data) ? $data['data'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
$contents = [
|
||||
'time' => Time::now()->getTimestamp(),
|
||||
'ttl' => $ttl,
|
||||
'data' => $value,
|
||||
];
|
||||
|
||||
if (write_file($this->path . $key, serialize($contents))) {
|
||||
try {
|
||||
chmod($this->path . $key, $this->mode);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Throwable $e) {
|
||||
log_message('debug', 'Failed to set mode on cache file: ' . $e);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return is_file($this->path . $key) && unlink($this->path . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
$deleted = 0;
|
||||
|
||||
foreach (glob($this->path . $pattern, GLOB_NOSORT) as $filename) {
|
||||
if (is_file($filename) && @unlink($filename)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
$prefixedKey = static::validateKey($key, $this->prefix);
|
||||
$tmp = $this->getItem($prefixedKey);
|
||||
|
||||
if ($tmp === false) {
|
||||
$tmp = ['data' => 0, 'ttl' => 60];
|
||||
}
|
||||
|
||||
['data' => $value, 'ttl' => $ttl] = $tmp;
|
||||
|
||||
if (! is_int($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value += $offset;
|
||||
|
||||
return $this->save($key, $value, $ttl) ? $value : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
return $this->increment($key, -$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
return delete_files($this->path, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return get_dir_file_info($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
if (false === $data = $this->getItem($key)) {
|
||||
return false; // @TODO This will return null in a future release
|
||||
}
|
||||
|
||||
return [
|
||||
'expire' => $data['ttl'] > 0 ? $data['time'] + $data['ttl'] : null,
|
||||
'mtime' => filemtime($this->path . $key),
|
||||
'data' => $data['data'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return is_writable($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the heavy lifting of actually retrieving the file and
|
||||
* verifying its age.
|
||||
*
|
||||
* @return array{data: mixed, ttl: int, time: int}|false
|
||||
*/
|
||||
protected function getItem(string $filename)
|
||||
{
|
||||
if (! is_file($this->path . $filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = @file_get_contents($this->path . $filename);
|
||||
|
||||
if ($content === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = unserialize($content);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! isset($data['ttl']) || ! is_int($data['ttl'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! isset($data['time']) || ! is_int($data['time'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($data['ttl'] > 0 && Time::now()->getTimestamp() > $data['time'] + $data['ttl']) {
|
||||
@unlink($this->path . $filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a file to disk, or returns false if not successful.
|
||||
*
|
||||
* @deprecated 4.6.1 Use `write_file()` instead.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $data
|
||||
* @param string $mode
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function writeFile($path, $data, $mode = 'wb')
|
||||
{
|
||||
if (($fp = @fopen($path, $mode)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flock($fp, LOCK_EX);
|
||||
|
||||
$result = 0;
|
||||
|
||||
for ($written = 0, $length = strlen($data); $written < $length; $written += $result) {
|
||||
if (($result = fwrite($fp, substr($data, $written))) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
|
||||
return is_int($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files contained in the supplied directory path.
|
||||
* Files must be writable or owned by the system in order to be deleted.
|
||||
* If the second parameter is set to TRUE, any directories contained
|
||||
* within the supplied base directory will be nuked as well.
|
||||
*
|
||||
* @deprecated 4.6.1 Use `delete_files()` instead.
|
||||
*
|
||||
* @param string $path File path
|
||||
* @param bool $delDir Whether to delete any directories found in the path
|
||||
* @param bool $htdocs Whether to skip deleting .htaccess and index page files
|
||||
* @param int $_level Current directory depth level (default: 0; internal use only)
|
||||
*/
|
||||
protected function deleteFiles(string $path, bool $delDir = false, bool $htdocs = false, int $_level = 0): bool
|
||||
{
|
||||
// Trim the trailing slash
|
||||
$path = rtrim($path, '/\\');
|
||||
|
||||
if (! $currentDir = @opendir($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (false !== ($filename = @readdir($currentDir))) {
|
||||
if ($filename !== '.' && $filename !== '..') {
|
||||
if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.') {
|
||||
$this->deleteFiles($path . DIRECTORY_SEPARATOR . $filename, $delDir, $htdocs, $_level + 1);
|
||||
} elseif (! $htdocs || preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename) !== 1) {
|
||||
@unlink($path . DIRECTORY_SEPARATOR . $filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir($currentDir);
|
||||
|
||||
return ($delDir && $_level > 0) ? @rmdir($path) : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the specified directory and builds an array containing the filenames,
|
||||
* filesize, dates, and permissions
|
||||
*
|
||||
* Any sub-folders contained within the specified path are read as well.
|
||||
*
|
||||
* @deprecated 4.6.1 Use `get_dir_file_info()` instead.
|
||||
*
|
||||
* @param string $sourceDir Path to source
|
||||
* @param bool $topLevelOnly Look only at the top level directory specified?
|
||||
* @param bool $_recursion Internal variable to determine recursion status - do not use in calls
|
||||
*
|
||||
* @return array<string, array{
|
||||
* name: string,
|
||||
* server_path: string,
|
||||
* size: int,
|
||||
* date: int,
|
||||
* relative_path: string,
|
||||
* }>|false
|
||||
*/
|
||||
protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false)
|
||||
{
|
||||
static $filedata = [];
|
||||
|
||||
$relativePath = $sourceDir;
|
||||
$filePointer = @opendir($sourceDir);
|
||||
|
||||
if (! is_bool($filePointer)) {
|
||||
// reset the array and make sure $sourceDir has a trailing slash on the initial call
|
||||
if ($_recursion === false) {
|
||||
$filedata = [];
|
||||
|
||||
$resolvedSrc = realpath($sourceDir);
|
||||
$resolvedSrc = $resolvedSrc === false ? $sourceDir : $resolvedSrc;
|
||||
|
||||
$sourceDir = rtrim($resolvedSrc, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
// Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast
|
||||
while (false !== $file = readdir($filePointer)) {
|
||||
if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) {
|
||||
$this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
|
||||
} elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') {
|
||||
$filedata[$file] = $this->getFileInfo($sourceDir . $file);
|
||||
|
||||
$filedata[$file]['relative_path'] = $relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
closedir($filePointer);
|
||||
|
||||
return $filedata;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a file and path, returns the name, path, size, date modified
|
||||
* Second parameter allows you to explicitly declare what information you want returned
|
||||
* Options are: name, server_path, size, date, readable, writable, executable, fileperms
|
||||
* Returns FALSE if the file cannot be found.
|
||||
*
|
||||
* @deprecated 4.6.1 Use `get_file_info()` instead.
|
||||
*
|
||||
* @param string $file Path to file
|
||||
* @param list<string>|string $returnedValues Array or comma separated string of information returned
|
||||
*
|
||||
* @return array{
|
||||
* name?: string,
|
||||
* server_path?: string,
|
||||
* size?: int,
|
||||
* date?: int,
|
||||
* readable?: bool,
|
||||
* writable?: bool,
|
||||
* executable?: bool,
|
||||
* fileperms?: int
|
||||
* }|false
|
||||
*/
|
||||
protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
|
||||
{
|
||||
if (! is_file($file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($returnedValues)) {
|
||||
$returnedValues = explode(',', $returnedValues);
|
||||
}
|
||||
|
||||
$fileInfo = [];
|
||||
|
||||
foreach ($returnedValues as $key) {
|
||||
switch ($key) {
|
||||
case 'name':
|
||||
$fileInfo['name'] = basename($file);
|
||||
break;
|
||||
|
||||
case 'server_path':
|
||||
$fileInfo['server_path'] = $file;
|
||||
break;
|
||||
|
||||
case 'size':
|
||||
$fileInfo['size'] = filesize($file);
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
$fileInfo['date'] = filemtime($file);
|
||||
break;
|
||||
|
||||
case 'readable':
|
||||
$fileInfo['readable'] = is_readable($file);
|
||||
break;
|
||||
|
||||
case 'writable':
|
||||
$fileInfo['writable'] = is_writable($file);
|
||||
break;
|
||||
|
||||
case 'executable':
|
||||
$fileInfo['executable'] = is_executable($file);
|
||||
break;
|
||||
|
||||
case 'fileperms':
|
||||
$fileInfo['fileperms'] = fileperms($file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $fileInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Cache;
|
||||
use Exception;
|
||||
use Memcache;
|
||||
use Memcached;
|
||||
|
||||
/**
|
||||
* Mamcached cache handler
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\MemcachedHandlerTest
|
||||
*/
|
||||
class MemcachedHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* The memcached object
|
||||
*
|
||||
* @var Memcache|Memcached
|
||||
*/
|
||||
protected $memcached;
|
||||
|
||||
/**
|
||||
* Memcached Configuration
|
||||
*
|
||||
* @var array{host: string, port: int, weight: int, raw: bool}
|
||||
*/
|
||||
protected $config = [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 11211,
|
||||
'weight' => 1,
|
||||
'raw' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* Note: Use `CacheFactory::getHandler()` to instantiate.
|
||||
*/
|
||||
public function __construct(Cache $config)
|
||||
{
|
||||
$this->prefix = $config->prefix;
|
||||
|
||||
$this->config = array_merge($this->config, $config->memcached);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection to Memcache(d) if present.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->memcached instanceof Memcached) {
|
||||
$this->memcached->quit();
|
||||
} elseif ($this->memcached instanceof Memcache) {
|
||||
$this->memcached->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
try {
|
||||
if (class_exists(Memcached::class)) {
|
||||
$this->memcached = new Memcached();
|
||||
|
||||
if ($this->config['raw']) {
|
||||
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
|
||||
}
|
||||
|
||||
$this->memcached->addServer(
|
||||
$this->config['host'],
|
||||
$this->config['port'],
|
||||
$this->config['weight'],
|
||||
);
|
||||
|
||||
$stats = $this->memcached->getStats();
|
||||
|
||||
// $stats should be an associate array with a key in the format of host:port.
|
||||
// If it doesn't have the key, we know the server is not working as expected.
|
||||
if (! is_array($stats) || ! isset($stats[$this->config['host'] . ':' . $this->config['port']])) {
|
||||
throw new CriticalError('Cache: Memcached connection failed.');
|
||||
}
|
||||
} elseif (class_exists(Memcache::class)) {
|
||||
$this->memcached = new Memcache();
|
||||
|
||||
if (! $this->memcached->connect($this->config['host'], $this->config['port'])) {
|
||||
throw new CriticalError('Cache: Memcache connection failed.');
|
||||
}
|
||||
|
||||
$this->memcached->addServer(
|
||||
$this->config['host'],
|
||||
$this->config['port'],
|
||||
true,
|
||||
$this->config['weight'],
|
||||
);
|
||||
} else {
|
||||
throw new CriticalError('Cache: Not support Memcache(d) extension.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw new CriticalError('Cache: Memcache(d) connection refused (' . $e->getMessage() . ').');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$data = [];
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
if ($this->memcached instanceof Memcached) {
|
||||
$data = $this->memcached->get($key);
|
||||
|
||||
// check for unmatched key
|
||||
if ($this->memcached->getResultCode() === Memcached::RES_NOTFOUND) {
|
||||
return null;
|
||||
}
|
||||
} elseif ($this->memcached instanceof Memcache) {
|
||||
$flags = false;
|
||||
$data = $this->memcached->get($key, $flags);
|
||||
|
||||
// check for unmatched key (i.e. $flags is untouched)
|
||||
if ($flags === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return is_array($data) ? $data[0] : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
if (! $this->config['raw']) {
|
||||
$value = [
|
||||
$value,
|
||||
Time::now()->getTimestamp(),
|
||||
$ttl,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->memcached instanceof Memcached) {
|
||||
return $this->memcached->set($key, $value, $ttl);
|
||||
}
|
||||
|
||||
if ($this->memcached instanceof Memcache) {
|
||||
return $this->memcached->set($key, $value, 0, $ttl);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return $this->memcached->delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
throw new BadMethodCallException('The deleteMatching method is not implemented for Memcached. You must select File, Redis or Predis handlers to use it.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
if (! $this->config['raw']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return $this->memcached->increment($key, $offset, $offset, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
if (! $this->config['raw']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
// FIXME: third parameter isn't other handler actions.
|
||||
|
||||
return $this->memcached->decrement($key, $offset, $offset, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
return $this->memcached->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return $this->memcached->getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
$stored = $this->memcached->get($key);
|
||||
|
||||
// if not an array, don't try to count for PHP7.2
|
||||
if (! is_array($stored) || count($stored) !== 3) {
|
||||
return false; // @TODO This will return null in a future release
|
||||
}
|
||||
|
||||
[$data, $time, $limit] = $stored;
|
||||
|
||||
return [
|
||||
'expire' => $limit > 0 ? $time + $limit : null,
|
||||
'mtime' => $time,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return extension_loaded('memcached') || extension_loaded('memcache');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Cache;
|
||||
use Exception;
|
||||
use Predis\Client;
|
||||
use Predis\Collection\Iterator\Keyspace;
|
||||
use Predis\Response\Status;
|
||||
|
||||
/**
|
||||
* Predis cache handler
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\PredisHandlerTest
|
||||
*/
|
||||
class PredisHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* Default config
|
||||
*
|
||||
* @var array{
|
||||
* scheme: string,
|
||||
* host: string,
|
||||
* password: string|null,
|
||||
* port: int,
|
||||
* timeout: int
|
||||
* }
|
||||
*/
|
||||
protected $config = [
|
||||
'scheme' => 'tcp',
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* Predis connection
|
||||
*
|
||||
* @var Client
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
/**
|
||||
* Note: Use `CacheFactory::getHandler()` to instantiate.
|
||||
*/
|
||||
public function __construct(Cache $config)
|
||||
{
|
||||
$this->prefix = $config->prefix;
|
||||
|
||||
if (isset($config->redis)) {
|
||||
$this->config = array_merge($this->config, $config->redis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
try {
|
||||
$this->redis = new Client($this->config, ['prefix' => $this->prefix]);
|
||||
$this->redis->time();
|
||||
} catch (Exception $e) {
|
||||
throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ').');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
$data = array_combine(
|
||||
['__ci_type', '__ci_value'],
|
||||
$this->redis->hmget($key, ['__ci_type', '__ci_value']),
|
||||
);
|
||||
|
||||
if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($data['__ci_type']) {
|
||||
'array', 'object' => unserialize($data['__ci_value']),
|
||||
// Yes, 'double' is returned and NOT 'float'
|
||||
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
switch ($dataType = gettype($value)) {
|
||||
case 'array':
|
||||
case 'object':
|
||||
$value = serialize($value);
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
case 'integer':
|
||||
case 'double': // Yes, 'double' is returned and NOT 'float'
|
||||
case 'string':
|
||||
case 'NULL':
|
||||
break;
|
||||
|
||||
case 'resource':
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->redis->hmset($key, ['__ci_type' => $dataType, '__ci_value' => $value]) instanceof Status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ttl !== 0) {
|
||||
$this->redis->expireat($key, Time::now()->getTimestamp() + $ttl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
return $this->redis->del($key) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
$matchedKeys = [];
|
||||
|
||||
foreach (new Keyspace($this->redis, $pattern) as $key) {
|
||||
$matchedKeys[] = $key;
|
||||
}
|
||||
|
||||
return $this->redis->del($matchedKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
return $this->redis->hincrby($key, 'data', $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
return $this->redis->hincrby($key, 'data', -$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 使用 deleteMatching 仅删除带缓存前缀的键,避免 flushdb 清空同库的 Session 等数据
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
$pattern = $this->prefix !== '' ? $this->prefix . '*' : '*';
|
||||
$deleted = $this->deleteMatching($pattern);
|
||||
return $deleted >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return $this->redis->info();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
$key = static::validateKey($key);
|
||||
|
||||
$data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value']));
|
||||
|
||||
if (isset($data['__ci_value']) && $data['__ci_value'] !== false) {
|
||||
$time = Time::now()->getTimestamp();
|
||||
$ttl = $this->redis->ttl($key);
|
||||
|
||||
return [
|
||||
'expire' => $ttl > 0 ? $time + $ttl : null,
|
||||
'mtime' => $time,
|
||||
'data' => $data['__ci_value'],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return class_exists(Client::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Cache;
|
||||
use Redis;
|
||||
use RedisException;
|
||||
|
||||
/**
|
||||
* Redis cache handler
|
||||
*
|
||||
* @see \CodeIgniter\Cache\Handlers\RedisHandlerTest
|
||||
*/
|
||||
class RedisHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* Default config
|
||||
*
|
||||
* @var array{
|
||||
* host: string,
|
||||
* password: string|null,
|
||||
* port: int,
|
||||
* timeout: int,
|
||||
* database: int,
|
||||
* }
|
||||
*/
|
||||
protected $config = [
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'database' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* Redis connection
|
||||
*
|
||||
* @var Redis|null
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
/**
|
||||
* Note: Use `CacheFactory::getHandler()` to instantiate.
|
||||
*/
|
||||
public function __construct(Cache $config)
|
||||
{
|
||||
$this->prefix = $config->prefix;
|
||||
|
||||
$this->config = array_merge($this->config, $config->redis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection to Redis if present.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->redis)) {
|
||||
$this->redis->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
$config = $this->config;
|
||||
|
||||
$this->redis = new Redis();
|
||||
|
||||
try {
|
||||
// Note:: If Redis is your primary cache choice, and it is "offline", every page load will end up been delayed by the timeout duration.
|
||||
// I feel like some sort of temporary flag should be set, to indicate that we think Redis is "offline", allowing us to bypass the timeout for a set period of time.
|
||||
|
||||
if (! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout'])) {
|
||||
// Note:: I'm unsure if log_message() is necessary, however I'm not 100% comfortable removing it.
|
||||
log_message('error', 'Cache: Redis connection failed. Check your configuration.');
|
||||
|
||||
throw new CriticalError('Cache: Redis connection failed. Check your configuration.');
|
||||
}
|
||||
|
||||
if (isset($config['password']) && ! $this->redis->auth($config['password'])) {
|
||||
log_message('error', 'Cache: Redis authentication failed.');
|
||||
|
||||
throw new CriticalError('Cache: Redis authentication failed.');
|
||||
}
|
||||
|
||||
if (isset($config['database']) && ! $this->redis->select($config['database'])) {
|
||||
log_message('error', 'Cache: Redis select database failed.');
|
||||
|
||||
throw new CriticalError('Cache: Redis select database failed.');
|
||||
}
|
||||
} catch (RedisException $e) {
|
||||
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
$data = $this->redis->hMget($key, ['__ci_type', '__ci_value']);
|
||||
|
||||
if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($data['__ci_type']) {
|
||||
'array', 'object' => unserialize($data['__ci_value']),
|
||||
// Yes, 'double' is returned and NOT 'float'
|
||||
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
switch ($dataType = gettype($value)) {
|
||||
case 'array':
|
||||
case 'object':
|
||||
$value = serialize($value);
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
case 'integer':
|
||||
case 'double': // Yes, 'double' is returned and NOT 'float'
|
||||
case 'string':
|
||||
case 'NULL':
|
||||
break;
|
||||
|
||||
case 'resource':
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->redis->hMset($key, ['__ci_type' => $dataType, '__ci_value' => $value])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ttl !== 0) {
|
||||
$this->redis->expireAt($key, Time::now()->getTimestamp() + $ttl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return $this->redis->del($key) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
/** @var list<string> $matchedKeys */
|
||||
$matchedKeys = [];
|
||||
$pattern = static::validateKey($pattern, $this->prefix);
|
||||
$iterator = null;
|
||||
|
||||
do {
|
||||
/** @var false|list<string> $keys */
|
||||
$keys = $this->redis->scan($iterator, $pattern);
|
||||
|
||||
if (is_array($keys)) {
|
||||
//$matchedKeys = [...$matchedKeys, ...$keys];
|
||||
$matchedKeys = array_merge($matchedKeys, $keys);
|
||||
}
|
||||
} while ($iterator > 0);
|
||||
|
||||
return $this->redis->del($matchedKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return $this->redis->hIncrBy($key, '__ci_value', $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
return $this->increment($key, -$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 使用 deleteMatching 仅删除带缓存前缀的键,避免 flushDB 清空同库的 Session 等数据
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
$deleted = $this->deleteMatching('*');
|
||||
return $deleted >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return $this->redis->info();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
$value = $this->get($key);
|
||||
|
||||
if ($value !== null) {
|
||||
$time = Time::now()->getTimestamp();
|
||||
$ttl = $this->redis->ttl(static::validateKey($key, $this->prefix));
|
||||
assert(is_int($ttl));
|
||||
|
||||
return [
|
||||
'expire' => $ttl > 0 ? $time + $ttl : null,
|
||||
'mtime' => $time,
|
||||
'data' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return extension_loaded('redis');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache\Handlers;
|
||||
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Cache;
|
||||
|
||||
/**
|
||||
* Cache handler for WinCache from Microsoft & IIS.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class WincacheHandler extends BaseHandler
|
||||
{
|
||||
/**
|
||||
* Note: Use `CacheFactory::getHandler()` to instantiate.
|
||||
*/
|
||||
public function __construct(Cache $config)
|
||||
{
|
||||
$this->prefix = $config->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
$success = false;
|
||||
|
||||
$data = wincache_ucache_get($key, $success);
|
||||
|
||||
// Success returned by reference from wincache_ucache_get()
|
||||
return $success ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function save(string $key, $value, int $ttl = 60)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return wincache_ucache_set($key, $value, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return wincache_ucache_delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function deleteMatching(string $pattern)
|
||||
{
|
||||
throw new BadMethodCallException('The deleteMatching method is not implemented for Wincache. You must select File, Redis or Predis handlers to use it.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function increment(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return wincache_ucache_inc($key, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function decrement(string $key, int $offset = 1)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
return wincache_ucache_dec($key, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clean()
|
||||
{
|
||||
return wincache_ucache_clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getCacheInfo()
|
||||
{
|
||||
return wincache_ucache_info(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getMetaData(string $key)
|
||||
{
|
||||
$key = static::validateKey($key, $this->prefix);
|
||||
|
||||
if ($stored = wincache_ucache_info(false, $key)) {
|
||||
$age = $stored['ucache_entries'][1]['age_seconds'];
|
||||
$ttl = $stored['ucache_entries'][1]['ttl_seconds'];
|
||||
$hitcount = $stored['ucache_entries'][1]['hitcount'];
|
||||
|
||||
return [
|
||||
'expire' => $ttl > 0 ? Time::now()->getTimestamp() + $ttl : null,
|
||||
'hitcount' => $hitcount,
|
||||
'age' => $age,
|
||||
'ttl' => $ttl,
|
||||
];
|
||||
}
|
||||
|
||||
return false; // @TODO This will return null in a future release
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return extension_loaded('wincache') && ini_get('wincache.ucenabled');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Cache;
|
||||
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
use CodeIgniter\HTTP\CLIRequest;
|
||||
use CodeIgniter\HTTP\Header;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Cache as CacheConfig;
|
||||
|
||||
/**
|
||||
* Web Page Caching
|
||||
*
|
||||
* @see \CodeIgniter\Cache\ResponseCacheTest
|
||||
*/
|
||||
final class ResponseCache
|
||||
{
|
||||
/**
|
||||
* Whether to take the URL query string into consideration when generating
|
||||
* output cache files. Valid options are:
|
||||
*
|
||||
* false = Disabled
|
||||
* true = Enabled, take all query parameters into account.
|
||||
* Please be aware that this may result in numerous cache
|
||||
* files generated for the same page over and over again.
|
||||
* array('q') = Enabled, but only take into account the specified list
|
||||
* of query parameters.
|
||||
*
|
||||
* @var bool|list<string>
|
||||
*/
|
||||
private array|bool $cacheQueryString = false;
|
||||
|
||||
/**
|
||||
* Cache time to live (TTL) in seconds.
|
||||
*/
|
||||
private int $ttl = 0;
|
||||
|
||||
public function __construct(CacheConfig $config, private readonly CacheInterface $cache)
|
||||
{
|
||||
$this->cacheQueryString = $config->cacheQueryString;
|
||||
}
|
||||
|
||||
public function setTtl(int $ttl): self
|
||||
{
|
||||
$this->ttl = $ttl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the cache key to use from the current request.
|
||||
*
|
||||
* @internal for testing purposes only
|
||||
*/
|
||||
public function generateCacheKey(CLIRequest|IncomingRequest $request): string
|
||||
{
|
||||
if ($request instanceof CLIRequest) {
|
||||
return md5($request->getPath());
|
||||
}
|
||||
|
||||
$uri = clone $request->getUri();
|
||||
|
||||
$query = (bool) $this->cacheQueryString
|
||||
? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : [])
|
||||
: '';
|
||||
|
||||
return md5($request->getMethod() . ':' . $uri->setFragment('')->setQuery($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the response.
|
||||
*/
|
||||
public function make(CLIRequest|IncomingRequest $request, ResponseInterface $response): bool
|
||||
{
|
||||
if ($this->ttl === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
foreach ($response->headers() as $name => $value) {
|
||||
if ($value instanceof Header) {
|
||||
$headers[$name] = $value->getValueLine();
|
||||
} else {
|
||||
foreach ($value as $header) {
|
||||
$headers[$name][] = $header->getValueLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cache->save(
|
||||
$this->generateCacheKey($request),
|
||||
serialize(['headers' => $headers, 'output' => $response->getBody()]),
|
||||
$this->ttl,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cached response for the request.
|
||||
*/
|
||||
public function get(CLIRequest|IncomingRequest $request, ResponseInterface $response): ?ResponseInterface
|
||||
{
|
||||
$cachedResponse = $this->cache->get($this->generateCacheKey($request));
|
||||
|
||||
if (is_string($cachedResponse) && $cachedResponse !== '') {
|
||||
$cachedResponse = unserialize($cachedResponse);
|
||||
|
||||
if (
|
||||
! is_array($cachedResponse)
|
||||
|| ! isset($cachedResponse['output'])
|
||||
|| ! isset($cachedResponse['headers'])
|
||||
) {
|
||||
throw new RuntimeException('Error unserializing page cache');
|
||||
}
|
||||
|
||||
$headers = $cachedResponse['headers'];
|
||||
$output = $cachedResponse['output'];
|
||||
|
||||
// Clear all default headers
|
||||
foreach (array_keys($response->headers()) as $key) {
|
||||
$response->removeHeader($key);
|
||||
}
|
||||
|
||||
// Set cached headers
|
||||
foreach ($headers as $name => $value) {
|
||||
$response->setHeader($name, $value);
|
||||
}
|
||||
|
||||
$response->setBody($output);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
use CodeIgniter\Autoloader\FileLocatorInterface;
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
use Config\Encryption;
|
||||
use Config\Modules;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* Class BaseConfig
|
||||
*
|
||||
* Not intended to be used on its own, this class will attempt to
|
||||
* automatically populate the child class' properties with values
|
||||
* from the environment.
|
||||
*
|
||||
* These can be set within the .env file.
|
||||
*
|
||||
* @phpstan-consistent-constructor
|
||||
* @see \CodeIgniter\Config\BaseConfigTest
|
||||
*/
|
||||
class BaseConfig
|
||||
{
|
||||
/**
|
||||
* An optional array of classes that will act as Registrars
|
||||
* for rapidly setting config class properties.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $registrars = [];
|
||||
|
||||
/**
|
||||
* Whether to override properties by Env vars and Registrars.
|
||||
*/
|
||||
public static $override = true;
|
||||
|
||||
/**
|
||||
* Has module discovery completed?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $didDiscovery = false;
|
||||
|
||||
/**
|
||||
* Is module discovery running or not?
|
||||
*/
|
||||
protected static $discovering = false;
|
||||
|
||||
/**
|
||||
* The processing Registrar file for error message.
|
||||
*/
|
||||
protected static $registrarFile = '';
|
||||
|
||||
/**
|
||||
* The modules configuration.
|
||||
*
|
||||
* @var Modules|null
|
||||
*/
|
||||
protected static $moduleConfig;
|
||||
|
||||
public static function __set_state(array $array)
|
||||
{
|
||||
static::$override = false;
|
||||
$obj = new static();
|
||||
static::$override = true;
|
||||
|
||||
$properties = array_keys(get_object_vars($obj));
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$obj->{$property} = $array[$property];
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal For testing purposes only.
|
||||
* @testTag
|
||||
*/
|
||||
public static function setModules(Modules $modules): void
|
||||
{
|
||||
static::$moduleConfig = $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal For testing purposes only.
|
||||
* @testTag
|
||||
*/
|
||||
public static function reset(): void
|
||||
{
|
||||
static::$registrars = [];
|
||||
static::$override = true;
|
||||
static::$didDiscovery = false;
|
||||
static::$moduleConfig = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attempt to get environment variables with names
|
||||
* that match the properties of the child class.
|
||||
*
|
||||
* The "shortPrefix" is the lowercase-only config class name.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
|
||||
if (! static::$override) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registerProperties();
|
||||
|
||||
$properties = array_keys(get_object_vars($this));
|
||||
$prefix = static::class;
|
||||
$slashAt = strrpos($prefix, '\\');
|
||||
$shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1));
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
|
||||
|
||||
if ($this instanceof Encryption && $property === 'key') {
|
||||
if (str_starts_with($this->{$property}, 'hex2bin:')) {
|
||||
// Handle hex2bin prefix
|
||||
$this->{$property} = hex2bin(substr($this->{$property}, 8));
|
||||
} elseif (str_starts_with($this->{$property}, 'base64:')) {
|
||||
// Handle base64 prefix
|
||||
$this->{$property} = base64_decode(substr($this->{$property}, 7), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization an environment-specific configuration setting
|
||||
*
|
||||
* @param array|bool|float|int|string|null $property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix)
|
||||
{
|
||||
if (is_array($property)) {
|
||||
foreach (array_keys($property) as $key) {
|
||||
$this->initEnvValue($property[$key], "{$name}.{$key}", $prefix, $shortPrefix);
|
||||
}
|
||||
} elseif (($value = $this->getEnvValue($name, $prefix, $shortPrefix)) !== false && $value !== null) {
|
||||
if ($value === 'false') {
|
||||
$value = false;
|
||||
} elseif ($value === 'true') {
|
||||
$value = true;
|
||||
}
|
||||
if (is_bool($value)) {
|
||||
$property = $value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$value = trim($value, '\'"');
|
||||
|
||||
if (is_int($property)) {
|
||||
$value = (int) $value;
|
||||
} elseif (is_float($property)) {
|
||||
$value = (float) $value;
|
||||
}
|
||||
|
||||
// If the default value of the property is `null` and the type is not
|
||||
// `string`, TypeError will happen.
|
||||
// So cannot set `declare(strict_types=1)` in this file.
|
||||
$property = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an environment-specific configuration setting
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getEnvValue(string $property, string $prefix, string $shortPrefix)
|
||||
{
|
||||
$shortPrefix = ltrim($shortPrefix, '\\');
|
||||
$underscoreProperty = str_replace('.', '_', $property);
|
||||
|
||||
switch (true) {
|
||||
case array_key_exists("{$shortPrefix}.{$property}", $_ENV):
|
||||
return $_ENV["{$shortPrefix}.{$property}"];
|
||||
|
||||
case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_ENV):
|
||||
return $_ENV["{$shortPrefix}_{$underscoreProperty}"];
|
||||
|
||||
case array_key_exists("{$shortPrefix}.{$property}", $_SERVER):
|
||||
return $_SERVER["{$shortPrefix}.{$property}"];
|
||||
|
||||
case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_SERVER):
|
||||
return $_SERVER["{$shortPrefix}_{$underscoreProperty}"];
|
||||
|
||||
case array_key_exists("{$prefix}.{$property}", $_ENV):
|
||||
return $_ENV["{$prefix}.{$property}"];
|
||||
|
||||
case array_key_exists("{$prefix}_{$underscoreProperty}", $_ENV):
|
||||
return $_ENV["{$prefix}_{$underscoreProperty}"];
|
||||
|
||||
case array_key_exists("{$prefix}.{$property}", $_SERVER):
|
||||
return $_SERVER["{$prefix}.{$property}"];
|
||||
|
||||
case array_key_exists("{$prefix}_{$underscoreProperty}", $_SERVER):
|
||||
return $_SERVER["{$prefix}_{$underscoreProperty}"];
|
||||
|
||||
default:
|
||||
$value = getenv("{$shortPrefix}.{$property}");
|
||||
$value = $value === false ? getenv("{$shortPrefix}_{$underscoreProperty}") : $value;
|
||||
$value = $value === false ? getenv("{$prefix}.{$property}") : $value;
|
||||
$value = $value === false ? getenv("{$prefix}_{$underscoreProperty}") : $value;
|
||||
|
||||
return $value === false ? null : $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides external libraries a simple way to register one or more
|
||||
* options into a config file.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
protected function registerProperties()
|
||||
{
|
||||
|
||||
|
||||
if (! static::$didDiscovery) {
|
||||
// Discovery must be completed before the first instantiation of any Config class.
|
||||
if (static::$discovering) {
|
||||
throw new ConfigException(
|
||||
'During Auto-Discovery of Registrars,'
|
||||
. ' "' . static::class . '" executes Auto-Discovery again.'
|
||||
. ' "' . clean_path(static::$registrarFile) . '" seems to have bad code.'
|
||||
);
|
||||
}
|
||||
|
||||
static::$discovering = true;
|
||||
|
||||
|
||||
|
||||
static::$didDiscovery = true;
|
||||
static::$discovering = false;
|
||||
}
|
||||
|
||||
$shortName = (new ReflectionClass($this))->getShortName();
|
||||
|
||||
// Check the registrar class for a method named after this class' shortName
|
||||
foreach (static::$registrars as $callable) {
|
||||
// ignore non-applicable registrars
|
||||
if (! method_exists($callable, $shortName)) {
|
||||
continue; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$properties = $callable::$shortName();
|
||||
|
||||
if (! is_array($properties)) {
|
||||
throw new RuntimeException('Registrars must return an array of properties and their values.');
|
||||
}
|
||||
|
||||
foreach ($properties as $property => $value) {
|
||||
if (isset($this->{$property}) && is_array($this->{$property}) && is_array($value)) {
|
||||
$this->{$property} = array_merge($this->{$property}, $value);
|
||||
} else {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
use CodeIgniter\Autoloader\Autoloader;
|
||||
use CodeIgniter\Autoloader\FileLocator;
|
||||
use CodeIgniter\Autoloader\FileLocatorCached;
|
||||
use CodeIgniter\Autoloader\FileLocatorInterface;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Cache\ResponseCache;
|
||||
use CodeIgniter\CLI\Commands;
|
||||
use CodeIgniter\CodeIgniter;
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use CodeIgniter\Database\MigrationRunner;
|
||||
use CodeIgniter\Debug\Exceptions;
|
||||
use CodeIgniter\Debug\Iterator;
|
||||
use CodeIgniter\Debug\Timer;
|
||||
use CodeIgniter\Debug\Toolbar;
|
||||
use CodeIgniter\Email\Email;
|
||||
use CodeIgniter\Encryption\EncrypterInterface;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use CodeIgniter\Filters\Filters;
|
||||
use CodeIgniter\Format\Format;
|
||||
use CodeIgniter\Honeypot\Honeypot;
|
||||
use CodeIgniter\HTTP\CLIRequest;
|
||||
use CodeIgniter\HTTP\ContentSecurityPolicy;
|
||||
use CodeIgniter\HTTP\CURLRequest;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\Negotiate;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\Request;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\HTTP\SiteURIFactory;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\Images\Handlers\BaseHandler;
|
||||
use CodeIgniter\Language\Language;
|
||||
use CodeIgniter\Log\Logger;
|
||||
use CodeIgniter\Pager\Pager;
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
use CodeIgniter\Router\RouteCollectionInterface;
|
||||
use CodeIgniter\Router\Router;
|
||||
use CodeIgniter\Security\Security;
|
||||
use CodeIgniter\Session\Session;
|
||||
use CodeIgniter\Superglobals;
|
||||
use CodeIgniter\Throttle\Throttler;
|
||||
use CodeIgniter\Typography\Typography;
|
||||
use CodeIgniter\Validation\ValidationInterface;
|
||||
use CodeIgniter\View\Cell;
|
||||
use CodeIgniter\View\Parser;
|
||||
use CodeIgniter\View\RendererInterface;
|
||||
use CodeIgniter\View\View;
|
||||
use Config\App;
|
||||
use Config\Autoload;
|
||||
use Config\Cache;
|
||||
use Config\ContentSecurityPolicy as CSPConfig;
|
||||
use Config\Encryption;
|
||||
use Config\Exceptions as ConfigExceptions;
|
||||
use Config\Filters as ConfigFilters;
|
||||
use Config\Format as ConfigFormat;
|
||||
use Config\Honeypot as ConfigHoneyPot;
|
||||
use Config\Images;
|
||||
use Config\Migrations;
|
||||
use Config\Modules;
|
||||
use Config\Optimize;
|
||||
use Config\Pager as ConfigPager;
|
||||
use CodeIgniter\Config\Services as AppServices;
|
||||
use Config\Session as ConfigSession;
|
||||
use Config\Toolbar as ConfigToolbar;
|
||||
use Config\Validation as ConfigValidation;
|
||||
use Config\View as ConfigView;
|
||||
|
||||
/**
|
||||
* Services Configuration file.
|
||||
*
|
||||
* Services are simply other classes/libraries that the system uses
|
||||
* to do its job. This is used by CodeIgniter to allow the core of the
|
||||
* framework to be swapped out easily without affecting the usage within
|
||||
* the rest of your application.
|
||||
*
|
||||
* This is used in place of a Dependency Injection container primarily
|
||||
* due to its simplicity, which allows a better long-term maintenance
|
||||
* of the applications built on top of CodeIgniter. A bonus side-effect
|
||||
* is that IDEs are able to determine what class you are calling
|
||||
* whereas with DI Containers there usually isn't a way for them to do this.
|
||||
*
|
||||
* Warning: To allow overrides by service providers do not use static calls,
|
||||
* instead call out to \Config\Services (imported as AppServices).
|
||||
*
|
||||
* @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html
|
||||
* @see http://www.infoq.com/presentations/Simple-Made-Easy
|
||||
*
|
||||
* @method static CacheInterface cache(Cache $config = null, $getShared = true)
|
||||
* @method static CLIRequest clirequest(App $config = null, $getShared = true)
|
||||
* @method static CodeIgniter codeigniter(App $config = null, $getShared = true)
|
||||
* @method static Commands commands($getShared = true)
|
||||
* @method static void createRequest(App $config, bool $isCli = false)
|
||||
* @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true)
|
||||
* @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true)
|
||||
* @method static Email email($config = null, $getShared = true)
|
||||
* @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false)
|
||||
* @method static Exceptions exceptions(ConfigExceptions $config = null, $getShared = true)
|
||||
* @method static Filters filters(ConfigFilters $config = null, $getShared = true)
|
||||
* @method static Format format(ConfigFormat $config = null, $getShared = true)
|
||||
* @method static Honeypot honeypot(ConfigHoneyPot $config = null, $getShared = true)
|
||||
* @method static BaseHandler image($handler = null, Images $config = null, $getShared = true)
|
||||
* @method static IncomingRequest incomingrequest(?App $config = null, bool $getShared = true)
|
||||
* @method static Iterator iterator($getShared = true)
|
||||
* @method static Language language($locale = null, $getShared = true)
|
||||
* @method static Logger logger($getShared = true)
|
||||
* @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true)
|
||||
* @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true)
|
||||
* @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true)
|
||||
* @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true)
|
||||
* @method static RedirectResponse redirectresponse(App $config = null, $getShared = true)
|
||||
* @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true)
|
||||
* @method static IncomingRequest|CLIRequest request(App $config = null, $getShared = true)
|
||||
* @method static ResponseInterface response(App $config = null, $getShared = true)
|
||||
* @method static ResponseCache responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true)
|
||||
* @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true)
|
||||
* @method static RouteCollection routes($getShared = true)
|
||||
* @method static Security security(App $config = null, $getShared = true)
|
||||
* @method static Session session(ConfigSession $config = null, $getShared = true)
|
||||
* @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true)
|
||||
* @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true)
|
||||
* @method static Throttler throttler($getShared = true)
|
||||
* @method static Timer timer($getShared = true)
|
||||
* @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true)
|
||||
* @method static Typography typography($getShared = true)
|
||||
* @method static URI uri($uri = null, $getShared = true)
|
||||
* @method static ValidationInterface validation(ConfigValidation $config = null, $getShared = true)
|
||||
* @method static Cell viewcell($getShared = true)
|
||||
*/
|
||||
class BaseService
|
||||
{
|
||||
/**
|
||||
* Cache for instance of any services that
|
||||
* have been requested as a "shared" instance.
|
||||
* Keys should be lowercase service names.
|
||||
*
|
||||
* @var array<string, object> [key => instance]
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* Factory method list.
|
||||
*
|
||||
* @var array<string, (callable(mixed ...$params): object)> [key => callable]
|
||||
*/
|
||||
protected static array $factories = [];
|
||||
|
||||
/**
|
||||
* Mock objects for testing which are returned if exist.
|
||||
*
|
||||
* @var array<string, object> [key => instance]
|
||||
*/
|
||||
protected static $mocks = [];
|
||||
|
||||
/**
|
||||
* Have we already discovered other Services?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $discovered = false;
|
||||
|
||||
/**
|
||||
* A cache of other service classes we've found.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @deprecated 4.5.0 No longer used.
|
||||
*/
|
||||
protected static $services = [];
|
||||
|
||||
/**
|
||||
* A cache of the names of services classes found.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private static array $serviceNames = [];
|
||||
|
||||
/**
|
||||
* Simple method to get an entry fast.
|
||||
*
|
||||
* @param string $key Identifier of the entry to look for.
|
||||
*
|
||||
* @return object|null Entry.
|
||||
*/
|
||||
public static function get(string $key): ?object
|
||||
{
|
||||
return static::$instances[$key] ?? static::__callStatic($key, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an entry.
|
||||
*
|
||||
* @param string $key Identifier of the entry.
|
||||
*/
|
||||
public static function set(string $key, object $value): void
|
||||
{
|
||||
if (isset(static::$instances[$key])) {
|
||||
throw new InvalidArgumentException('The entry for "' . $key . '" is already set.');
|
||||
}
|
||||
|
||||
static::$instances[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides an existing entry.
|
||||
*
|
||||
* @param string $key Identifier of the entry.
|
||||
*/
|
||||
public static function override(string $key, object $value): void
|
||||
{
|
||||
static::$instances[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shared instance of any of the class' services.
|
||||
*
|
||||
* $key must be a name matching a service.
|
||||
*
|
||||
* @param array|bool|float|int|object|string|null ...$params
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
protected static function getSharedInstance(string $key, ...$params)
|
||||
{
|
||||
$key = strtolower($key);
|
||||
|
||||
// Returns mock if exists
|
||||
if (isset(static::$mocks[$key])) {
|
||||
return static::$mocks[$key];
|
||||
}
|
||||
|
||||
if (! isset(static::$instances[$key])) {
|
||||
// Make sure $getShared is false
|
||||
$params[] = false;
|
||||
|
||||
static::$instances[$key] = AppServices::$key(...$params);
|
||||
}
|
||||
|
||||
return static::$instances[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* The Autoloader class is the central class that handles our
|
||||
* spl_autoload_register method, and helper methods.
|
||||
*
|
||||
* @return Autoloader
|
||||
*/
|
||||
public static function autoloader(bool $getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
if (empty(static::$instances['autoloader'])) {
|
||||
static::$instances['autoloader'] = new Autoloader();
|
||||
}
|
||||
|
||||
return static::$instances['autoloader'];
|
||||
}
|
||||
|
||||
return new Autoloader();
|
||||
}
|
||||
|
||||
/**
|
||||
* The file locator provides utility methods for looking for non-classes
|
||||
* within namespaced folders, as well as convenience methods for
|
||||
* loading 'helpers', and 'libraries'.
|
||||
*
|
||||
* @return FileLocatorInterface
|
||||
*/
|
||||
public static function locator(bool $getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
if (empty(static::$instances['locator'])) {
|
||||
$cacheEnabled = class_exists(Optimize::class)
|
||||
&& (new Optimize())->locatorCacheEnabled;
|
||||
|
||||
if ($cacheEnabled) {
|
||||
static::$instances['locator'] = new FileLocatorCached(new FileLocator(static::autoloader()));
|
||||
} else {
|
||||
static::$instances['locator'] = new FileLocator(static::autoloader());
|
||||
}
|
||||
}
|
||||
|
||||
return static::$mocks['locator'] ?? static::$instances['locator'];
|
||||
}
|
||||
|
||||
return new FileLocator(static::autoloader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the ability to perform case-insensitive calling of service
|
||||
* names.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
public static function __callStatic(string $name, array $arguments)
|
||||
{
|
||||
if (isset(static::$factories[$name])) {
|
||||
return static::$factories[$name](...$arguments);
|
||||
}
|
||||
|
||||
$service = static::serviceExists($name);
|
||||
|
||||
if ($service === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $service::$name(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the requested service is defined and return the declaring
|
||||
* class. Return null if not found.
|
||||
*/
|
||||
public static function serviceExists(string $name): ?string
|
||||
{
|
||||
static::buildServicesCache();
|
||||
|
||||
$services = array_merge(self::$serviceNames, [Services::class]);
|
||||
$name = strtolower($name);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (method_exists($service, $name)) {
|
||||
static::$factories[$name] = [$service, $name];
|
||||
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset shared instances and mocks for testing.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @testTag only available to test code
|
||||
*/
|
||||
public static function reset(bool $initAutoloader = true)
|
||||
{
|
||||
static::$mocks = [];
|
||||
static::$instances = [];
|
||||
static::$factories = [];
|
||||
|
||||
if ($initAutoloader) {
|
||||
static::autoloader()->initialize(new Autoload(), new Modules());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets any mock and shared instances for a single service.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @testTag only available to test code
|
||||
*/
|
||||
public static function resetSingle(string $name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
unset(static::$mocks[$name], static::$instances[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject mock object for testing.
|
||||
*
|
||||
* @param object $mock
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @testTag only available to test code
|
||||
*/
|
||||
public static function injectMock(string $name, $mock)
|
||||
{
|
||||
static::$instances[$name] = $mock;
|
||||
static::$mocks[strtolower($name)] = $mock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the service cache.
|
||||
*/
|
||||
public static function resetServicesCache(): void
|
||||
{
|
||||
self::$serviceNames = [];
|
||||
static::$discovered = false;
|
||||
}
|
||||
|
||||
protected static function buildServicesCache(): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php namespace Config;
|
||||
/**
|
||||
* {{www.xunruicms.com}}
|
||||
* {{迅睿内容管理框架系统}}
|
||||
* 本文件是框架系统文件,二次开发时不可以修改本文件
|
||||
**/
|
||||
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||
use CodeIgniter\Cache\Handlers\PredisHandler;
|
||||
use CodeIgniter\Cache\Handlers\RedisHandler;
|
||||
use CodeIgniter\Cache\Handlers\WincacheHandler;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class Cache extends BaseConfig
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Primary Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the preferred handler that should be used. If for some reason
|
||||
| it is not available, the $backupHandler will be used in its place.
|
||||
|
|
||||
*/
|
||||
public $handler = SYS_CACHE_TYPE == 1 ? 'memcached' : (SYS_CACHE_TYPE == 2 ? 'redis' : 'file');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Backup Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the handler that will be used in case the first one is
|
||||
| unreachable. Often, 'file' is used here since the filesystem is
|
||||
| always available, though that's not always practical for the app.
|
||||
|
|
||||
*/
|
||||
public $backupHandler = 'file';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Directory Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The path to where cache files should be stored, if using a file-based
|
||||
| system.
|
||||
|
|
||||
*/
|
||||
public $storePath = WRITEPATH.'file/';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Include Query String
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether to take the URL query string into consideration when generating
|
||||
| output cache files. Valid options are:
|
||||
|
|
||||
| false = Disabled
|
||||
| true = Enabled, take all query parameters into account.
|
||||
| Please be aware that this may result in numerous cache
|
||||
| files generated for the same page over and over again.
|
||||
| array('q') = Enabled, but only take into account the specified list
|
||||
| of query parameters.
|
||||
|
|
||||
*/
|
||||
public $cacheQueryString = false;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This string is added to all cache item names to help avoid collisions
|
||||
| if you run multiple applications with the same cache engine.
|
||||
|
|
||||
*/
|
||||
public string $prefix = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Default TTL
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The default number of seconds to save items when none is specified.
|
||||
*
|
||||
* WARNING: This is not used by framework handlers where 60 seconds is
|
||||
* hard-coded, but may be useful to projects and modules. This will replace
|
||||
* the hard-coded value in a future release.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public int $ttl = 600;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Reserved Characters
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* A string of reserved characters that will not be allowed in keys or tags.
|
||||
* Strings that violate this restriction will cause handlers to throw.
|
||||
* Default: {}()/\@:
|
||||
* Note: The default set is required for PSR-6 compliance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $reservedCharacters = '{}()/\@:';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* File settings
|
||||
* --------------------------------------------------------------------------
|
||||
* Your file storage preferences can be specified below, if you are using
|
||||
* the File driver.
|
||||
*
|
||||
* @var array<string, int|string|null>
|
||||
*/
|
||||
public array $file = [
|
||||
'storePath' => WRITEPATH . 'file/',
|
||||
'mode' => 0640,
|
||||
];
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------------
|
||||
| Memcached settings
|
||||
| -------------------------------------------------------------------------
|
||||
| Your Memcached servers can be specified below, if you are using
|
||||
| the Memcached drivers.
|
||||
|
|
||||
| See: https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||
|
|
||||
*/
|
||||
public array $memcached = [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 11211,
|
||||
'weight' => 1,
|
||||
'raw' => false,
|
||||
];
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------------
|
||||
| Redis settings
|
||||
| -------------------------------------------------------------------------
|
||||
| Your Redis server can be specified below, if you are using
|
||||
| the Redis or Predis drivers.
|
||||
|
|
||||
*/
|
||||
public array $redis = [
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'database' => 0,
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Available Cache Handlers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is an array of cache engine alias' and class names. Only engines
|
||||
| that are listed here are allowed to be used.
|
||||
|
|
||||
*/
|
||||
public array $validHandlers = [
|
||||
'dummy' => DummyHandler::class,
|
||||
'file' => FileHandler::class,
|
||||
'memcached' => MemcachedHandler::class,
|
||||
'predis' => PredisHandler::class,
|
||||
'redis' => RedisHandler::class,
|
||||
'wincache' => WincacheHandler::class,
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
if (is_file(CONFIGPATH.'redis.php')) {
|
||||
$this->redis = require CONFIGPATH.'redis.php';
|
||||
} elseif (is_file(ROOTPATH.'config/redis.php')) {
|
||||
$this->redis = require ROOTPATH.'config/redis.php';
|
||||
}
|
||||
if (is_file(CONFIGPATH.'memcached.php')) {
|
||||
$this->memcached = require CONFIGPATH.'memcached.php';
|
||||
} elseif (is_file(ROOTPATH.'config/memcached.php')) {
|
||||
$this->memcached = require ROOTPATH.'config/memcached.php';
|
||||
}
|
||||
$this->prefix = substr(SYS_KEY, 0, 10).'-';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php namespace Config;
|
||||
/**
|
||||
* {{www.xunruicms.com}}
|
||||
* {{迅睿内容管理框架系统}}
|
||||
* 本文件是框架系统文件,二次开发时不可以修改本文件
|
||||
**/
|
||||
|
||||
use CodeIgniter\Database\Config;
|
||||
|
||||
/**
|
||||
* Database Configuration
|
||||
*/
|
||||
class Database extends Config
|
||||
{
|
||||
/**
|
||||
* The directory that holds the Migrations
|
||||
* and Seeds directories.
|
||||
* @var string
|
||||
*/
|
||||
public $filesPath = WRITEPATH.'database/';
|
||||
|
||||
/**
|
||||
* Lets you choose which connection group to
|
||||
* use if no other is specified.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $defaultGroup = 'default';
|
||||
|
||||
/**
|
||||
* The default database connection.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $default = [
|
||||
'DSN' => '',
|
||||
'hostname' => 'localhost',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'database' => '',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => '',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => true,
|
||||
'cacheOn' => true,
|
||||
'cacheDir' => WRITEPATH.'database/',
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_general_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'foreignKeys' => true,
|
||||
'numberNative' => false,
|
||||
'failover' => []
|
||||
];
|
||||
public $db1 = [];
|
||||
public $db2 = [];
|
||||
public $db3 = [];
|
||||
public $db4 = [];
|
||||
public $db5 = [];
|
||||
public $db6 = [];
|
||||
public $db7 = [];
|
||||
public $db8 = [];
|
||||
public $db9 = [];
|
||||
|
||||
private $mykey = 1;
|
||||
private $mydb = [];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!is_file(CONFIGPATH.'database.php')) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$db = [];
|
||||
require CONFIGPATH.'database.php';
|
||||
|
||||
if (isset($db['failover']) && $db['failover']) {
|
||||
// 备用库
|
||||
$this->default['failover'] = $db['failover'];
|
||||
unset($db['failover']);
|
||||
}
|
||||
|
||||
foreach ($this->default as $p => $t) {
|
||||
foreach ($db as $name => $v) {
|
||||
if (isset($this->$name)) {
|
||||
// 默认库
|
||||
$this->$name[$p] = isset($v[$p]) ? $v[$p] : $t;
|
||||
} else {
|
||||
// 自定义库
|
||||
$key = $this->_get_key($name);
|
||||
if ($key) {
|
||||
$this->$key[$p] = isset($v[$p]) ? $v[$p] : $t;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断数据库名称的规范性(SQLite3 为文件路径,允许 .db 等扩展名)
|
||||
$driver = isset($this->default['DBDriver']) ? (string) $this->default['DBDriver'] : 'MySQLi';
|
||||
if (strcasecmp($driver, 'SQLite3') !== 0) {
|
||||
if (is_numeric($this->default['database'])) {
|
||||
exit('数据库名称不能是数字');
|
||||
} elseif (strpos((string) $this->default['database'], '.') !== false) {
|
||||
exit('数据库名称不能存在.号');
|
||||
}
|
||||
} else {
|
||||
// SQLite 默认等待写锁,降低 database is locked 概率
|
||||
if (!isset($this->default['busyTimeout'])) {
|
||||
$this->default['busyTimeout'] = 10000;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function _get_key($name) {
|
||||
if (isset($this->mydb[$name])) {
|
||||
return $this->mydb[$name];
|
||||
} else {
|
||||
$this->mydb[$name] = 'db'.$this->mykey;
|
||||
$this->mykey++;
|
||||
}
|
||||
return $this->mydb[$name];
|
||||
}
|
||||
|
||||
public function get_group($name) {
|
||||
if (isset($this->mydb[$name])) {
|
||||
return $this->mydb[$name];
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php namespace Config;
|
||||
/**
|
||||
* {{www.xunruicms.com}}
|
||||
* {{迅睿内容管理框架系统}}
|
||||
* 本文件是框架系统文件,二次开发时不可以修改本文件
|
||||
**/
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Debug\ExceptionHandler;
|
||||
use CodeIgniter\Debug\ExceptionHandlerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Setup how the exception handler works.
|
||||
*/
|
||||
class Exceptions extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* LOG EXCEPTIONS?
|
||||
* --------------------------------------------------------------------------
|
||||
* If true, then exceptions will be logged
|
||||
* through Services::Log.
|
||||
*
|
||||
* Default: true
|
||||
*/
|
||||
public bool $log = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* DO NOT LOG STATUS CODES
|
||||
* --------------------------------------------------------------------------
|
||||
* Any status codes here will NOT be logged if logging is turned on.
|
||||
* By default, only 404 (Page Not Found) exceptions are ignored.
|
||||
*/
|
||||
public array $ignoreCodes = [404];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Error Views Path
|
||||
* --------------------------------------------------------------------------
|
||||
* This is the path to the directory that contains the 'cli' and 'html'
|
||||
* directories that hold the views used to generate errors.
|
||||
*
|
||||
* Default: APPPATH.'Views/errors'
|
||||
*/
|
||||
public $errorViewPath = FRAMEPATH . 'Exceptions/Views';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* HIDE FROM DEBUG TRACE
|
||||
* --------------------------------------------------------------------------
|
||||
* Any data that you would like to hide from the debug trace.
|
||||
* In order to specify 2 levels, use "/" to separate.
|
||||
* ex. ['server', 'setup/password', 'secret_token']
|
||||
*/
|
||||
public array $sensitiveDataInTrace = [];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* LOG DEPRECATIONS INSTEAD OF THROWING?
|
||||
* --------------------------------------------------------------------------
|
||||
* By default, CodeIgniter converts deprecations into exceptions. Also,
|
||||
* starting in PHP 8.1 will cause a lot of deprecated usage warnings.
|
||||
* Use this option to temporarily cease the warnings and instead log those.
|
||||
* This option also works for user deprecations.
|
||||
*/
|
||||
public bool $logDeprecations = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
|
||||
* --------------------------------------------------------------------------
|
||||
* If `$logDeprecations` is set to `true`, this sets the log level
|
||||
* to which the deprecation will be logged. This should be one of the log
|
||||
* levels recognized by PSR-3.
|
||||
*
|
||||
* The related `Config\Logger::$threshold` should be adjusted, if needed,
|
||||
* to capture logging the deprecations.
|
||||
*/
|
||||
public string $deprecationLogLevel = 'warning';
|
||||
|
||||
/*
|
||||
* DEFINE THE HANDLERS USED
|
||||
* --------------------------------------------------------------------------
|
||||
* Given the HTTP status code, returns exception handler that
|
||||
* should be used to deal with this error. By default, it will run CodeIgniter's
|
||||
* default handler and display the error information in the expected format
|
||||
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
|
||||
* response format.
|
||||
*
|
||||
* Custom handlers can be returned if you want to handle one or more specific
|
||||
* error codes yourself like:
|
||||
*
|
||||
* if (in_array($statusCode, [400, 404, 500])) {
|
||||
* return new \App\Libraries\MyExceptionHandler();
|
||||
* }
|
||||
* if ($exception instanceOf PageNotFoundException) {
|
||||
* return new \App\Libraries\MyExceptionHandler();
|
||||
* }
|
||||
*/
|
||||
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
|
||||
{
|
||||
return new ExceptionHandler($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
use CodeIgniter\Autoloader\FileLocatorInterface;
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use CodeIgniter\Model;
|
||||
|
||||
/**
|
||||
* Factories for creating instances.
|
||||
*
|
||||
* Factories allow dynamic loading of components by their path
|
||||
* and name. The "shared instance" implementation provides a
|
||||
* large performance boost and helps keep code clean of lengthy
|
||||
* instantiation checks.
|
||||
*
|
||||
* @method static BaseConfig|null config(...$arguments)
|
||||
* @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null)
|
||||
* @see \CodeIgniter\Config\FactoriesTest
|
||||
*/
|
||||
final class Factories
|
||||
{
|
||||
/**
|
||||
* Store of component-specific options, usually
|
||||
* from CodeIgniter\Config\Factory.
|
||||
*
|
||||
* @var array<string, array<string, bool|string|null>>
|
||||
*/
|
||||
private static $options = [];
|
||||
|
||||
/**
|
||||
* Explicit options for the Config
|
||||
* component to prevent logic loops.
|
||||
*
|
||||
* @var array<string, bool|string|null>
|
||||
*/
|
||||
private static $configOptions = [
|
||||
'component' => 'config',
|
||||
'path' => 'Config',
|
||||
'instanceOf' => null,
|
||||
'getShared' => true,
|
||||
'preferApp' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of class aliases to their true Fully Qualified Class Name (FQCN).
|
||||
*
|
||||
* Class aliases can be:
|
||||
* - FQCN. E.g., 'App\Lib\SomeLib'
|
||||
* - short classname. E.g., 'SomeLib'
|
||||
* - short classname with sub-directories. E.g., 'Sub/SomeLib'
|
||||
*
|
||||
* [component => [alias => FQCN]]
|
||||
*
|
||||
* @var array<string, array<string, class-string>>
|
||||
*/
|
||||
private static $aliases = [];
|
||||
|
||||
/**
|
||||
* Store for instances of any component that
|
||||
* has been requested as "shared".
|
||||
*
|
||||
* A multi-dimensional array with components as
|
||||
* keys to the array of name-indexed instances.
|
||||
*
|
||||
* [component => [FQCN => instance]]
|
||||
*
|
||||
* @var array<string, array<class-string, object>>
|
||||
*/
|
||||
private static $instances = [];
|
||||
|
||||
/**
|
||||
* Whether the component instances are updated?
|
||||
*
|
||||
* @var array<string, true> [component => true]
|
||||
*
|
||||
* @internal For caching only
|
||||
*/
|
||||
private static $updated = [];
|
||||
|
||||
/**
|
||||
* Define the class to load. You can *override* the concrete class.
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
* @param class-string $classname FQCN to be loaded
|
||||
*/
|
||||
public static function define(string $component, string $alias, string $classname): void
|
||||
{
|
||||
$component = strtolower($component);
|
||||
|
||||
if (isset(self::$aliases[$component][$alias])) {
|
||||
if (self::$aliases[$component][$alias] === $classname) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = 'Already defined in Factories: ' . $component . ' ' . $alias . ' -> ' . self::$aliases[$component][$alias];
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
if (! class_exists($classname)) {
|
||||
throw new InvalidArgumentException('No such class: ' . $classname);
|
||||
}
|
||||
|
||||
// Force a configuration to exist for this component.
|
||||
// Otherwise, getOptions() will reset the component.
|
||||
self::getOptions($component);
|
||||
|
||||
self::$aliases[$component][$alias] = $classname;
|
||||
self::$updated[$component] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads instances based on the method component name. Either
|
||||
* creates a new instance or returns an existing shared instance.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
public static function __callStatic(string $component, array $arguments)
|
||||
{
|
||||
$component = strtolower($component);
|
||||
|
||||
// First argument is the class alias, second is options
|
||||
$alias = trim(array_shift($arguments), '\\ ');
|
||||
$options = array_shift($arguments) ?? [];
|
||||
|
||||
// Determine the component-specific options
|
||||
$options = array_merge(self::getOptions($component), $options);
|
||||
|
||||
if (! $options['getShared']) {
|
||||
if (isset(self::$aliases[$options['component']][$alias])) {
|
||||
$class = self::$aliases[$options['component']][$alias];
|
||||
|
||||
return new $class(...$arguments);
|
||||
}
|
||||
|
||||
// Try to locate the class
|
||||
$class = self::locateClass($options, $alias);
|
||||
if ($class !== null) {
|
||||
return new $class(...$arguments);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for an existing definition
|
||||
$instance = self::getDefinedInstance($options, $alias, $arguments);
|
||||
if ($instance !== null) {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
// Try to locate the class
|
||||
if (($class = self::locateClass($options, $alias)) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
self::createInstance($options['component'], $class, $arguments);
|
||||
self::setAlias($options['component'], $alias, $class);
|
||||
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple method to get the shared instance fast.
|
||||
*/
|
||||
public static function get(string $component, string $alias): ?object
|
||||
{
|
||||
if (isset(self::$aliases[$component][$alias])) {
|
||||
$class = self::$aliases[$component][$alias];
|
||||
|
||||
if (isset(self::$instances[$component][$class])) {
|
||||
return self::$instances[$component][$class];
|
||||
}
|
||||
}
|
||||
|
||||
return self::__callStatic($component, [$alias]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the defined instance. If not exists, creates new one.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
private static function getDefinedInstance(array $options, string $alias, array $arguments)
|
||||
{
|
||||
// The alias is already defined.
|
||||
if (isset(self::$aliases[$options['component']][$alias])) {
|
||||
$class = self::$aliases[$options['component']][$alias];
|
||||
|
||||
// Need to verify if the shared instance matches the request
|
||||
if (self::verifyInstanceOf($options, $class)) {
|
||||
// Check for an existing instance
|
||||
if (isset(self::$instances[$options['component']][$class])) {
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
|
||||
self::createInstance($options['component'], $class, $arguments);
|
||||
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to locate the class
|
||||
if (($class = self::locateClass($options, $alias)) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for an existing instance for the class
|
||||
if (isset(self::$instances[$options['component']][$class])) {
|
||||
self::setAlias($options['component'], $alias, $class);
|
||||
|
||||
return self::$instances[$options['component']][$class];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the shared instance.
|
||||
*/
|
||||
private static function createInstance(string $component, string $class, array $arguments): void
|
||||
{
|
||||
self::$instances[$component][$class] = new $class(...$arguments);
|
||||
self::$updated[$component] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets alias
|
||||
*/
|
||||
private static function setAlias(string $component, string $alias, string $class): void
|
||||
{
|
||||
self::$aliases[$component][$alias] = $class;
|
||||
self::$updated[$component] = true;
|
||||
|
||||
// If a short classname is specified, also register FQCN to share the instance.
|
||||
if (! isset(self::$aliases[$component][$class]) && ! self::isNamespaced($alias)) {
|
||||
self::$aliases[$component][$class] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the component Config?
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
*/
|
||||
private static function isConfig(string $component): bool
|
||||
{
|
||||
return $component === 'config';
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a component class
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
*/
|
||||
private static function locateClass(array $options, string $alias): ?string
|
||||
{
|
||||
// Check for low-hanging fruit
|
||||
if (
|
||||
class_exists($alias, false)
|
||||
&& self::verifyPreferApp($options, $alias)
|
||||
&& self::verifyInstanceOf($options, $alias)
|
||||
) {
|
||||
return $alias;
|
||||
}
|
||||
|
||||
// Determine the relative class names we need
|
||||
$basename = self::getBasename($alias);
|
||||
$appname = self::isConfig($options['component'])
|
||||
? 'Config\\' . $basename
|
||||
: rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
|
||||
|
||||
// If an App version was requested then see if it verifies
|
||||
if (
|
||||
// preferApp is used only for no namespaced class.
|
||||
! self::isNamespaced($alias)
|
||||
&& $options['preferApp'] && class_exists($appname)
|
||||
&& self::verifyInstanceOf($options, $alias)
|
||||
) {
|
||||
return $appname;
|
||||
}
|
||||
|
||||
// If we have ruled out an App version and the class exists then try it
|
||||
if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) {
|
||||
return $alias;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the class alias namespaced or not?
|
||||
*
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
*/
|
||||
private static function isNamespaced(string $alias): bool
|
||||
{
|
||||
return str_contains($alias, '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a class & config satisfy the "preferApp" option
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
*/
|
||||
private static function verifyPreferApp(array $options, string $alias): bool
|
||||
{
|
||||
// Anything without that restriction passes
|
||||
if (! $options['preferApp']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case for Config since its App namespace is actually \Config
|
||||
if (self::isConfig($options['component'])) {
|
||||
return str_starts_with($alias, 'Config');
|
||||
}
|
||||
|
||||
return str_starts_with($alias, APP_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a class & config satisfy the "instanceOf" option
|
||||
*
|
||||
* @param array $options The array of component-specific directives
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
*/
|
||||
private static function verifyInstanceOf(array $options, string $alias): bool
|
||||
{
|
||||
// Anything without that restriction passes
|
||||
if (! $options['instanceOf']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_a($alias, $options['instanceOf'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component-specific configuration
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
*
|
||||
* @return array<string, bool|string|null>
|
||||
*
|
||||
* @internal For testing only
|
||||
* @testTag
|
||||
*/
|
||||
public static function getOptions(string $component): array
|
||||
{
|
||||
$component = strtolower($component);
|
||||
|
||||
// Check for a stored version
|
||||
if (isset(self::$options[$component])) {
|
||||
return self::$options[$component];
|
||||
}
|
||||
|
||||
$values = self::isConfig($component)
|
||||
// Handle Config as a special case to prevent logic loops
|
||||
? self::$configOptions
|
||||
// Load values from the best Factory configuration (will include Registrars)
|
||||
: config('Factory')->{$component} ?? [];
|
||||
|
||||
// The setOptions() reset the component. So getOptions() may reset
|
||||
// the component.
|
||||
return self::setOptions($component, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes, stores, and returns the configuration for a specific component
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
* @param array $values option values
|
||||
*
|
||||
* @return array<string, bool|string|null> The result after applying defaults and normalization
|
||||
*/
|
||||
public static function setOptions(string $component, array $values): array
|
||||
{
|
||||
$component = strtolower($component);
|
||||
|
||||
// Allow the config to replace the component name, to support "aliases"
|
||||
$values['component'] = strtolower($values['component'] ?? $component);
|
||||
|
||||
// Reset this component so instances can be rediscovered with the updated config
|
||||
self::reset($values['component']);
|
||||
|
||||
// If no path was available then use the component
|
||||
$values['path'] = trim($values['path'] ?? ucfirst($values['component']), '\\ ');
|
||||
|
||||
// Add defaults for any missing values
|
||||
$values = array_merge(Factory::$default, $values);
|
||||
|
||||
// Store the result to the supplied name and potential alias
|
||||
self::$options[$component] = $values;
|
||||
self::$options[$values['component']] = $values;
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the static arrays, optionally just for one component
|
||||
*
|
||||
* @param string|null $component Lowercase, plural component name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function reset(?string $component = null)
|
||||
{
|
||||
if ($component !== null) {
|
||||
unset(
|
||||
self::$options[$component],
|
||||
self::$aliases[$component],
|
||||
self::$instances[$component],
|
||||
self::$updated[$component]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self::$options = [];
|
||||
self::$aliases = [];
|
||||
self::$instances = [];
|
||||
self::$updated = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for injecting mock instances
|
||||
*
|
||||
* @param string $component Lowercase, plural component name
|
||||
* @param string $alias Class alias. See the $aliases property.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @internal For testing only
|
||||
* @testTag
|
||||
*/
|
||||
public static function injectMock(string $component, string $alias, object $instance)
|
||||
{
|
||||
$component = strtolower($component);
|
||||
|
||||
// Force a configuration to exist for this component
|
||||
self::getOptions($component);
|
||||
|
||||
//$class = $instance::class;
|
||||
$class = get_class($instance);
|
||||
|
||||
self::$instances[$component][$class] = $instance;
|
||||
self::$aliases[$component][$alias] = $class;
|
||||
|
||||
if (self::isConfig($component)) {
|
||||
if (self::isNamespaced($alias)) {
|
||||
self::$aliases[$component][self::getBasename($alias)] = $class;
|
||||
} else {
|
||||
self::$aliases[$component]['Config\\' . $alias] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a basename from a class alias, namespaced or not.
|
||||
*
|
||||
* @internal For testing only
|
||||
* @testTag
|
||||
*/
|
||||
public static function getBasename(string $alias): string
|
||||
{
|
||||
// Determine the basename
|
||||
if ($basename = strrchr($alias, '\\')) {
|
||||
return substr($basename, 1);
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets component data for caching.
|
||||
*
|
||||
* @return array{
|
||||
* options: array<string, bool|string|null>,
|
||||
* aliases: array<string, class-string>,
|
||||
* instances: array<class-string, object>,
|
||||
* }
|
||||
*
|
||||
* @internal For caching only
|
||||
*/
|
||||
public static function getComponentInstances(string $component): array
|
||||
{
|
||||
if (! isset(self::$aliases[$component])) {
|
||||
return [
|
||||
'options' => [],
|
||||
'aliases' => [],
|
||||
'instances' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'options' => self::$options[$component],
|
||||
'aliases' => self::$aliases[$component],
|
||||
'instances' => self::$instances[$component],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets component data
|
||||
*
|
||||
* @internal For caching only
|
||||
*/
|
||||
public static function setComponentInstances(string $component, array $data): void
|
||||
{
|
||||
self::$options[$component] = $data['options'];
|
||||
self::$aliases[$component] = $data['aliases'];
|
||||
self::$instances[$component] = $data['instances'];
|
||||
|
||||
unset(self::$updated[$component]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the component instances are updated?
|
||||
*
|
||||
* @internal For caching only
|
||||
*/
|
||||
public static function isUpdated(string $component): bool
|
||||
{
|
||||
return isset(self::$updated[$component]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
/**
|
||||
* Factories Configuration file.
|
||||
*
|
||||
* Provides overriding directives for how
|
||||
* Factories should handle discovery and
|
||||
* instantiation of specific components.
|
||||
* Each property should correspond to the
|
||||
* lowercase, plural component name.
|
||||
*/
|
||||
class Factory extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* Supplies a default set of options to merge for
|
||||
* all unspecified factory components.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $default = [
|
||||
'component' => null,
|
||||
'path' => null,
|
||||
'instanceOf' => null,
|
||||
'getShared' => true,
|
||||
'preferApp' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies that Models should always favor child
|
||||
* classes to allow easy extension of module Models.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $models = [
|
||||
'preferApp' => true,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
/**
|
||||
* Describes foreign characters for transliteration with the text helper.
|
||||
*/
|
||||
class ForeignCharacters
|
||||
{
|
||||
/**
|
||||
* The list of foreign characters.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $characterList = [
|
||||
'/ä|æ|ǽ/' => 'ae',
|
||||
'/ö|œ/' => 'oe',
|
||||
'/ü/' => 'ue',
|
||||
'/Ä/' => 'Ae',
|
||||
'/Ü/' => 'Ue',
|
||||
'/Ö/' => 'Oe',
|
||||
'/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A',
|
||||
'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a',
|
||||
'/Б/' => 'B',
|
||||
'/б/' => 'b',
|
||||
'/Ç|Ć|Ĉ|Ċ|Č/' => 'C',
|
||||
'/ç|ć|ĉ|ċ|č/' => 'c',
|
||||
'/Д/' => 'D',
|
||||
'/д/' => 'd',
|
||||
'/Ð|Ď|Đ|Δ/' => 'Dj',
|
||||
'/ð|ď|đ|δ/' => 'dj',
|
||||
'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E',
|
||||
'/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e',
|
||||
'/Ф/' => 'F',
|
||||
'/ф/' => 'f',
|
||||
'/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G',
|
||||
'/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g',
|
||||
'/Ĥ|Ħ/' => 'H',
|
||||
'/ĥ|ħ/' => 'h',
|
||||
'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I',
|
||||
'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i',
|
||||
'/Ĵ/' => 'J',
|
||||
'/ĵ/' => 'j',
|
||||
'/Ķ|Κ|К/' => 'K',
|
||||
'/ķ|κ|к/' => 'k',
|
||||
'/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L',
|
||||
'/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l',
|
||||
'/М/' => 'M',
|
||||
'/м/' => 'm',
|
||||
'/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N',
|
||||
'/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n',
|
||||
'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O',
|
||||
'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o',
|
||||
'/П/' => 'P',
|
||||
'/п/' => 'p',
|
||||
'/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R',
|
||||
'/ŕ|ŗ|ř|ρ|р/' => 'r',
|
||||
'/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S',
|
||||
'/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's',
|
||||
'/Ț|Ţ|Ť|Ŧ|τ|Т/' => 'T',
|
||||
'/ț|ţ|ť|ŧ|т/' => 't',
|
||||
'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U',
|
||||
'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u',
|
||||
'/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y',
|
||||
'/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y',
|
||||
'/В/' => 'V',
|
||||
'/в/' => 'v',
|
||||
'/Ŵ/' => 'W',
|
||||
'/ŵ/' => 'w',
|
||||
'/Ź|Ż|Ž|Ζ|З/' => 'Z',
|
||||
'/ź|ż|ž|ζ|з/' => 'z',
|
||||
'/Æ|Ǽ/' => 'AE',
|
||||
'/ß/' => 'ss',
|
||||
'/IJ/' => 'IJ',
|
||||
'/ij/' => 'ij',
|
||||
'/Œ/' => 'OE',
|
||||
'/ƒ/' => 'f',
|
||||
'/ξ/' => 'ks',
|
||||
'/π/' => 'p',
|
||||
'/β/' => 'v',
|
||||
'/μ/' => 'm',
|
||||
'/ψ/' => 'ps',
|
||||
'/Ё/' => 'Yo',
|
||||
'/ё/' => 'yo',
|
||||
'/Є/' => 'Ye',
|
||||
'/є/' => 'ye',
|
||||
'/Ї/' => 'Yi',
|
||||
'/Ж/' => 'Zh',
|
||||
'/ж/' => 'zh',
|
||||
'/Х/' => 'Kh',
|
||||
'/х/' => 'kh',
|
||||
'/Ц/' => 'Ts',
|
||||
'/ц/' => 'ts',
|
||||
'/Ч/' => 'Ch',
|
||||
'/ч/' => 'ch',
|
||||
'/Ш/' => 'Sh',
|
||||
'/ш/' => 'sh',
|
||||
'/Щ/' => 'Shch',
|
||||
'/щ/' => 'shch',
|
||||
'/Ъ|ъ|Ь|ь/' => '',
|
||||
'/Ю/' => 'Yu',
|
||||
'/ю/' => 'yu',
|
||||
'/Я/' => 'Ya',
|
||||
'/я/' => 'ya',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
/**
|
||||
* Publisher Configuration
|
||||
*
|
||||
* Defines basic security restrictions for the Publisher class
|
||||
* to prevent abuse by injecting malicious files into a project.
|
||||
*/
|
||||
class Publisher extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* A list of allowed destinations with a (pseudo-)regex
|
||||
* of allowed files for each destination.
|
||||
* Attempts to publish to directories not in this list will
|
||||
* result in a PublisherException. Files that do no fit the
|
||||
* pattern will cause copy/merge to fail.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $restrictions = [
|
||||
ROOTPATH => '*',
|
||||
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
|
||||
];
|
||||
|
||||
/**
|
||||
* Disables Registrars to prevent modules from altering the restrictions.
|
||||
*/
|
||||
final protected function registerProperties(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Config;
|
||||
|
||||
use CodeIgniter\Cache\CacheFactory;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Debug\Exceptions;
|
||||
use Config\Cache;
|
||||
use Config\Exceptions as ExceptionsConfig;
|
||||
|
||||
/**
|
||||
* Services Configuration file.
|
||||
*
|
||||
* Services are simply other classes/libraries that the system uses
|
||||
* to do its job. This is used by CodeIgniter to allow the core of the
|
||||
* framework to be swapped out easily without affecting the usage within
|
||||
* the rest of your application.
|
||||
*
|
||||
* This is used in place of a Dependency Injection container primarily
|
||||
* due to its simplicity, which allows a better long-term maintenance
|
||||
* of the applications built on top of CodeIgniter. A bonus side-effect
|
||||
* is that IDEs are able to determine what class you are calling
|
||||
* whereas with DI Containers there usually isn't a way for them to do this.
|
||||
*
|
||||
* @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html
|
||||
* @see http://www.infoq.com/presentations/Simple-Made-Easy
|
||||
* @see \CodeIgniter\Config\ServicesTest
|
||||
*/
|
||||
class Services extends BaseService
|
||||
{
|
||||
/**
|
||||
* The cache class provides a simple way to store and retrieve
|
||||
* complex data for later.
|
||||
*
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public static function cache(?Cache $config = null, bool $getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('cache', $config);
|
||||
}
|
||||
|
||||
$config ??= config(Cache::class);
|
||||
|
||||
return CacheFactory::getHandler($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Exceptions class holds the methods that handle:
|
||||
*
|
||||
* - set_exception_handler
|
||||
* - set_error_handler
|
||||
* - register_shutdown_function
|
||||
*
|
||||
* @return Exceptions
|
||||
*/
|
||||
public static function exceptions(
|
||||
?ExceptionsConfig $config = null,
|
||||
bool $getShared = true,
|
||||
) {
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('exceptions', $config);
|
||||
}
|
||||
|
||||
$config ??= config(ExceptionsConfig::class);
|
||||
|
||||
return new Exceptions($config);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php namespace Config;
|
||||
/**
|
||||
* {{www.xunruicms.com}}
|
||||
* {{迅睿内容管理框架系统}}
|
||||
* 本文件是框架系统文件,二次开发时不可以修改本文件
|
||||
**/
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Database;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Files;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Routes;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Views;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Debug Toolbar
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The Debug Toolbar provides a way to see information about the performance
|
||||
* and state of your application during that page display. By default it will
|
||||
* NOT be displayed under production environments, and will only display if
|
||||
* `CI_DEBUG` is true, since if it's not, there's not much to display anyway.
|
||||
*/
|
||||
class Toolbar extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Toolbar Collectors
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* List of toolbar collectors that will be called when Debug Toolbar
|
||||
* fires up and collects data from.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $collectors = [
|
||||
Database::class,
|
||||
Views::class,
|
||||
Files::class,
|
||||
Routes::class
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Max History
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* `$maxHistory` sets a limit on the number of past requests that are stored,
|
||||
* helping to conserve file space used to store them. You can set it to
|
||||
* 0 (zero) to not have any history stored, or -1 for unlimited history.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $maxHistory = 20;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Toolbar Views Path
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* The full path to the the views that are used by the toolbar.
|
||||
* This MUST have a trailing slash.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $viewsPath = FRAMEPATH . 'Debug/Toolbar/Views/';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Max Queries
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If the Database Collector is enabled, it will log every query that the
|
||||
* the system generates so they can be displayed on the toolbar's timeline
|
||||
* and in the query log. This can lead to memory issues in some instances
|
||||
* with hundreds of queries.
|
||||
*
|
||||
* `$maxQueries` defines the maximum amount of queries that will be stored.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $maxQueries = 100;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Collect Var Data
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* If set to false var data from the views will not be colleted. Usefull to
|
||||
* avoid high memory usage when there are lots of data passed to the view.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collectVarData = true;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Watched Directories
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Contains an array of directories that will be watched for changes and
|
||||
* used to determine if the hot-reload feature should reload the page or not.
|
||||
* We restrict the values to keep performance as high as possible.
|
||||
*
|
||||
* NOTE: The ROOTPATH will be prepended to all values.
|
||||
*/
|
||||
public $watchedDirectories = [
|
||||
'app',
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Watched File Extensions
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Contains an array of file extensions that will be watched for changes and
|
||||
* used to determine if the hot-reload feature should reload the page or not.
|
||||
*/
|
||||
public $watchedExtensions = [
|
||||
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
|
||||
];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use ArgumentCountError;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use ErrorException;
|
||||
|
||||
/**
|
||||
* @template TConnection
|
||||
* @template TStatement
|
||||
* @template TResult
|
||||
*
|
||||
* @implements PreparedQueryInterface<TConnection, TStatement, TResult>
|
||||
*/
|
||||
abstract class BasePreparedQuery implements PreparedQueryInterface
|
||||
{
|
||||
/**
|
||||
* The prepared statement itself.
|
||||
*
|
||||
* @var TStatement|null
|
||||
*/
|
||||
protected $statement;
|
||||
|
||||
/**
|
||||
* The error code, if any.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $errorCode;
|
||||
|
||||
/**
|
||||
* The error message, if any.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $errorString;
|
||||
|
||||
/**
|
||||
* Holds the prepared query object
|
||||
* that is cloned during execute.
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* A reference to the db connection to use.
|
||||
*
|
||||
* @var BaseConnection<TConnection, TResult>
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prepare(string $sql, array $options = [], string $queryClass = Query::class)
|
||||
{
|
||||
// We only supports positional placeholders (?)
|
||||
// in order to work with the execute method below, so we
|
||||
// need to replace our named placeholders (:name)
|
||||
$sql = preg_replace('/:[^\s,)]+/', '?', $sql);
|
||||
|
||||
/** @var Query $query */
|
||||
$query = new $queryClass($this->db);
|
||||
|
||||
$query->setQuery($sql);
|
||||
|
||||
if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
|
||||
$query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
|
||||
}
|
||||
|
||||
$this->query = $query;
|
||||
|
||||
return $this->_prepare($query->getOriginalQuery(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The database-dependent portion of the prepare statement.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function _prepare(string $sql, array $options = []);
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*
|
||||
* @return bool|ResultInterface<TConnection, TResult>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function execute(...$data)
|
||||
{
|
||||
// Execute the Query.
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$exception = null;
|
||||
$result = $this->_execute($data);
|
||||
} catch (ArgumentCountError|ErrorException $exception) {
|
||||
$result = false;
|
||||
}
|
||||
|
||||
// Update our query object
|
||||
$query = clone $this->query;
|
||||
$query->setBinds($data);
|
||||
|
||||
if ($result === false) {
|
||||
$query->setDuration($startTime, $startTime);
|
||||
|
||||
// This will trigger a rollback if transactions are being used
|
||||
$this->db->handleTransStatus();
|
||||
|
||||
if ($this->db->DBDebug) {
|
||||
// We call this function in order to roll-back queries
|
||||
// if transactions are enabled. If we don't call this here
|
||||
// the error message will trigger an exit, causing the
|
||||
// transactions to remain in limbo.
|
||||
while ($this->db->transDepth !== 0) {
|
||||
$transDepth = $this->db->transDepth;
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($transDepth === $this->db->transDepth) {
|
||||
log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Let others do something with this query.
|
||||
Events::trigger('DBQuery', $query);
|
||||
|
||||
if ($exception !== null) {
|
||||
throw new DatabaseException($exception->getMessage(), $exception->getCode(), $exception);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let others do something with this query.
|
||||
Events::trigger('DBQuery', $query);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query->setDuration($startTime);
|
||||
|
||||
// Let others do something with this query
|
||||
Events::trigger('DBQuery', $query);
|
||||
|
||||
if ($this->db->isWriteType((string) $query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return a result object
|
||||
$resultClass = str_replace('PreparedQuery', 'Result', static::class);
|
||||
|
||||
$resultID = $this->_getResult();
|
||||
|
||||
return new $resultClass($this->db->connID, $resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* The database dependant version of the execute method.
|
||||
*/
|
||||
abstract public function _execute(array $data): bool;
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return object|resource|null
|
||||
*/
|
||||
abstract public function _getResult();
|
||||
|
||||
/**
|
||||
* Explicitly closes the prepared statement.
|
||||
*
|
||||
* @throws BadMethodCallException
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('Cannot call close on a non-existing prepared statement.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->_close();
|
||||
} finally {
|
||||
$this->statement = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The database-dependent version of the close method.
|
||||
*/
|
||||
abstract protected function _close(): bool;
|
||||
|
||||
/**
|
||||
* Returns the SQL that has been prepared.
|
||||
*/
|
||||
public function getQueryString(): string
|
||||
{
|
||||
if (! $this->query instanceof QueryInterface) {
|
||||
throw new BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.');
|
||||
}
|
||||
|
||||
return $this->query->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine if any error exists.
|
||||
*/
|
||||
public function hasError(): bool
|
||||
{
|
||||
return ! empty($this->errorString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return $this->errorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the input contain binary data.
|
||||
*/
|
||||
protected function isBinary(string $input): bool
|
||||
{
|
||||
return mb_detect_encoding($input, 'UTF-8', true) === false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @template TConnection
|
||||
* @template TResult
|
||||
*
|
||||
* @implements ResultInterface<TConnection, TResult>
|
||||
*/
|
||||
abstract class BaseResult implements ResultInterface
|
||||
{
|
||||
/**
|
||||
* Connection ID
|
||||
*
|
||||
* @var TConnection
|
||||
*/
|
||||
public $connID;
|
||||
|
||||
/**
|
||||
* Result ID
|
||||
*
|
||||
* @var false|TResult
|
||||
*/
|
||||
public $resultID;
|
||||
|
||||
/**
|
||||
* Result Array
|
||||
*
|
||||
* @var list<array>
|
||||
*/
|
||||
public $resultArray = [];
|
||||
|
||||
/**
|
||||
* Result Object
|
||||
*
|
||||
* @var list<object>
|
||||
*/
|
||||
public $resultObject = [];
|
||||
|
||||
/**
|
||||
* Custom Result Object
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $customResultObject = [];
|
||||
|
||||
/**
|
||||
* Current Row index
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $currentRow = 0;
|
||||
|
||||
/**
|
||||
* The number of records in the query result
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $numRows;
|
||||
|
||||
/**
|
||||
* Row data
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
public $rowData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param TConnection $connID
|
||||
* @param TResult $resultID
|
||||
*/
|
||||
public function __construct(&$connID, &$resultID)
|
||||
{
|
||||
$this->connID = $connID;
|
||||
$this->resultID = $resultID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the results of the query. Typically an array of
|
||||
* individual data rows, which can be either an 'array', an
|
||||
* 'object', or a custom class name.
|
||||
*
|
||||
* @param string $type The row type. Either 'array', 'object', or a class name to use
|
||||
*/
|
||||
public function getResult(string $type = 'object'): array
|
||||
{
|
||||
if ($type === 'array') {
|
||||
return $this->getResultArray();
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->getResultObject();
|
||||
}
|
||||
|
||||
return $this->getCustomResultObject($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of custom objects.
|
||||
*
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomResultObject(string $className)
|
||||
{
|
||||
if (isset($this->customResultObject[$className])) {
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
if (! $this->isValidResultId()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Don't fetch the result set again if we already have it
|
||||
$_data = null;
|
||||
if (($c = count($this->resultArray)) > 0) {
|
||||
$_data = 'resultArray';
|
||||
} elseif (($c = count($this->resultObject)) > 0) {
|
||||
$_data = 'resultObject';
|
||||
}
|
||||
|
||||
if ($_data !== null) {
|
||||
for ($i = 0; $i < $c; $i++) {
|
||||
$this->customResultObject[$className][$i] = new $className();
|
||||
|
||||
foreach ($this->{$_data}[$i] as $key => $value) {
|
||||
$this->customResultObject[$className][$i]->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
$this->customResultObject[$className] = [];
|
||||
|
||||
while ($row = $this->fetchObject($className)) {
|
||||
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
|
||||
$row->syncOriginal();
|
||||
}
|
||||
|
||||
$this->customResultObject[$className][] = $row;
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of arrays.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultArray(): array
|
||||
{
|
||||
if ($this->resultArray !== []) {
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
// In the event that query caching is on, the result_id variable
|
||||
// will not be a valid resource so we'll simply return an empty
|
||||
// array.
|
||||
if (! $this->isValidResultId()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->resultObject !== []) {
|
||||
foreach ($this->resultObject as $row) {
|
||||
$this->resultArray[] = (array) $row;
|
||||
}
|
||||
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
|
||||
while ($row = $this->fetchAssoc()) {
|
||||
$this->resultArray[] = $row;
|
||||
}
|
||||
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of objects.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*
|
||||
* @return list<stdClass>
|
||||
*/
|
||||
public function getResultObject(): array
|
||||
{
|
||||
if ($this->resultObject !== []) {
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
// In the event that query caching is on, the result_id variable
|
||||
// will not be a valid resource so we'll simply return an empty
|
||||
// array.
|
||||
if (! $this->isValidResultId()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->resultArray !== []) {
|
||||
foreach ($this->resultArray as $row) {
|
||||
$this->resultObject[] = (object) $row;
|
||||
}
|
||||
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
|
||||
while ($row = $this->fetchObject()) {
|
||||
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
|
||||
$row->syncOriginal();
|
||||
}
|
||||
|
||||
$this->resultObject[] = $row;
|
||||
}
|
||||
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object to return a row as either an array, an object, or
|
||||
* a custom class.
|
||||
*
|
||||
* If the row doesn't exist, returns null.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int|string $n The index of the results to return, or column name.
|
||||
* @param 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
|
||||
*/
|
||||
public function getRow($n = 0, string $type = 'object')
|
||||
{
|
||||
// $n is a column name.
|
||||
if (! is_numeric($n)) {
|
||||
// We cache the row data for subsequent uses
|
||||
if (! is_array($this->rowData)) {
|
||||
$this->rowData = $this->getRowArray();
|
||||
}
|
||||
|
||||
// array_key_exists() instead of isset() to allow for NULL values
|
||||
if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->rowData[$n];
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->getRowObject($n);
|
||||
}
|
||||
|
||||
if ($type === 'array') {
|
||||
return $this->getRowArray($n);
|
||||
}
|
||||
|
||||
return $this->getCustomRowObject($n, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a row as a custom class instance.
|
||||
*
|
||||
* If the row doesn't exist, returns null.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int $n The index of the results to return.
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function getCustomRowObject(int $n, string $className)
|
||||
{
|
||||
if (! isset($this->customResultObject[$className])) {
|
||||
$this->getCustomResultObject($className);
|
||||
}
|
||||
|
||||
if (empty($this->customResultObject[$className])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className][$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an array.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getRowArray(int $n = 0)
|
||||
{
|
||||
$result = $this->getResultArray();
|
||||
if ($result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->currentRow && isset($result[$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an object.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return object|stdClass|null
|
||||
*/
|
||||
public function getRowObject(int $n = 0)
|
||||
{
|
||||
$result = $this->getResultObject();
|
||||
if ($result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->customResultObject && isset($result[$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns an item into a particular column slot.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param array|object|stdClass|null $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRow($key, $value = null)
|
||||
{
|
||||
// We cache the row data for subsequent uses
|
||||
if (! is_array($this->rowData)) {
|
||||
$this->rowData = $this->getRowArray();
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
foreach ($key as $k => $v) {
|
||||
$this->rowData[$k] = $v;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key !== '' && $value !== null) {
|
||||
$this->rowData[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "first" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getFirstRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
|
||||
return ($result === []) ? null : $result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "last" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getLastRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
|
||||
return ($result === []) ? null : $result[count($result) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "next" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getNextRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
if ($result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "previous" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getPreviousRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
if ($result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($result[$this->currentRow - 1])) {
|
||||
$this->currentRow--;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered row and move the pointer to the next row.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getUnbufferedRow(string $type = 'object')
|
||||
{
|
||||
if ($type === 'array') {
|
||||
return $this->fetchAssoc();
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->fetchObject();
|
||||
}
|
||||
|
||||
return $this->fetchObject($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of rows in the result set; checks for previous count, falls
|
||||
* back on counting resultArray or resultObject, finally fetching resultArray
|
||||
* if nothing was previously fetched
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (is_int($this->numRows)) {
|
||||
return $this->numRows;
|
||||
}
|
||||
if ($this->resultArray !== []) {
|
||||
return $this->numRows = count($this->resultArray);
|
||||
}
|
||||
if ($this->resultObject !== []) {
|
||||
return $this->numRows = count($this->resultObject);
|
||||
}
|
||||
|
||||
return $this->numRows = count($this->getResultArray());
|
||||
}
|
||||
|
||||
private function isValidResultId(): bool
|
||||
{
|
||||
return is_resource($this->resultID) || is_object($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
abstract public function getFieldCount(): int;
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
abstract public function getFieldNames(): array;
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
abstract public function getFieldData(): array;
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function freeResult();
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function dataSeek(int $n = 0);
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return array|false|null
|
||||
*/
|
||||
abstract protected function fetchAssoc();
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return false|object
|
||||
*/
|
||||
abstract protected function fetchObject(string $className = stdClass::class);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Class BaseUtils
|
||||
*/
|
||||
abstract class BaseUtils
|
||||
{
|
||||
/**
|
||||
* Database object
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $listDatabases = false;
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $optimizeTable = false;
|
||||
|
||||
/**
|
||||
* REPAIR TABLE statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $repairTable = false;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*/
|
||||
public function __construct(ConnectionInterface $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* List databases
|
||||
*
|
||||
* @return array|bool
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function listDatabases()
|
||||
{
|
||||
// Is there a cached result?
|
||||
if (isset($this->db->dataCache['db_names'])) {
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
if ($this->listDatabases === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->dataCache['db_names'] = [];
|
||||
|
||||
$query = $this->db->query($this->listDatabases);
|
||||
if ($query === false) {
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i++) {
|
||||
$this->db->dataCache['db_names'][] = current($query[$i]);
|
||||
}
|
||||
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a particular database exists
|
||||
*/
|
||||
public function databaseExists(string $databaseName): bool
|
||||
{
|
||||
return in_array($databaseName, $this->listDatabases(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize Table
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function optimizeTable(string $tableName)
|
||||
{
|
||||
if ($this->optimizeTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
|
||||
|
||||
return $query !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize Database
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function optimizeDatabase()
|
||||
{
|
||||
if ($this->optimizeTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->db->listTables() as $tableName) {
|
||||
$res = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
|
||||
if (is_bool($res)) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
// Build the result array...
|
||||
|
||||
$res = $res->getResultArray();
|
||||
|
||||
// Postgre & SQLite3 returns empty array
|
||||
if (empty($res)) {
|
||||
$key = $tableName;
|
||||
} else {
|
||||
$res = current($res);
|
||||
$key = str_replace($this->db->database . '.', '', current($res));
|
||||
$keys = array_keys($res);
|
||||
unset($res[$keys[0]]);
|
||||
}
|
||||
|
||||
$result[$key] = $res;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair Table
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function repairTable(string $tableName)
|
||||
{
|
||||
if ($this->repairTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->db->query(sprintf($this->repairTable, $this->db->escapeIdentifiers($tableName)));
|
||||
if (is_bool($query)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$query = $query->getResultArray();
|
||||
|
||||
return current($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSV from a query result object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCSVFromResult(ResultInterface $query, string $delim = ',', string $newline = "\n", string $enclosure = '"')
|
||||
{
|
||||
$out = '';
|
||||
|
||||
foreach ($query->getFieldNames() as $name) {
|
||||
$out .= $enclosure . str_replace($enclosure, $enclosure . $enclosure, $name) . $enclosure . $delim;
|
||||
}
|
||||
|
||||
$out = substr($out, 0, -strlen($delim)) . $newline;
|
||||
|
||||
// Next blast through the result array and build out the rows
|
||||
while ($row = $query->getUnbufferedRow('array')) {
|
||||
$line = [];
|
||||
|
||||
foreach ($row as $item) {
|
||||
$line[] = $enclosure . str_replace(
|
||||
$enclosure,
|
||||
$enclosure . $enclosure,
|
||||
(string) $item,
|
||||
) . $enclosure;
|
||||
}
|
||||
|
||||
$out .= implode($delim, $line) . $newline;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XML data from a query result object
|
||||
*/
|
||||
public function getXMLFromResult(ResultInterface $query, array $params = []): string
|
||||
{
|
||||
foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) {
|
||||
if (! isset($params[$key])) {
|
||||
$params[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$root = $params['root'];
|
||||
$newline = $params['newline'];
|
||||
$tab = $params['tab'];
|
||||
$element = $params['element'];
|
||||
|
||||
helper('xml');
|
||||
$xml = '<' . $root . '>' . $newline;
|
||||
|
||||
while ($row = $query->getUnbufferedRow()) {
|
||||
$xml .= $tab . '<' . $element . '>' . $newline;
|
||||
|
||||
foreach ($row as $key => $val) {
|
||||
$val = (! empty($val)) ? xml_convert((string) $val) : '';
|
||||
|
||||
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
|
||||
}
|
||||
|
||||
$xml .= $tab . '</' . $element . '>' . $newline;
|
||||
}
|
||||
|
||||
return $xml . '</' . $root . '>' . $newline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database Backup
|
||||
*
|
||||
* @param array|string $params
|
||||
*
|
||||
* @return false|never|string
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function backup($params = [])
|
||||
{
|
||||
if (is_string($params)) {
|
||||
$params = ['tables' => $params];
|
||||
}
|
||||
|
||||
$prefs = [
|
||||
'tables' => [],
|
||||
'ignore' => [],
|
||||
'filename' => '',
|
||||
'format' => 'gzip', // gzip, txt
|
||||
'add_drop' => true,
|
||||
'add_insert' => true,
|
||||
'newline' => "\n",
|
||||
'foreign_key_checks' => true,
|
||||
];
|
||||
|
||||
if (! empty($params)) {
|
||||
foreach (array_keys($prefs) as $key) {
|
||||
if (isset($params[$key])) {
|
||||
$prefs[$key] = $params[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($prefs['tables'])) {
|
||||
$prefs['tables'] = $this->db->listTables();
|
||||
}
|
||||
|
||||
if (! in_array($prefs['format'], ['gzip', 'txt'], true)) {
|
||||
$prefs['format'] = 'txt';
|
||||
}
|
||||
|
||||
if ($prefs['format'] === 'gzip' && ! function_exists('gzencode')) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('The file compression format you chose is not supported by your server.');
|
||||
}
|
||||
|
||||
$prefs['format'] = 'txt';
|
||||
}
|
||||
|
||||
if ($prefs['format'] === 'txt') {
|
||||
return $this->_backup($prefs);
|
||||
}
|
||||
|
||||
// @TODO gzencode() requires `ext-zlib`, but _backup() is not implemented in all databases.
|
||||
return gzencode($this->_backup($prefs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return false|never|string
|
||||
*/
|
||||
abstract public function _backup(?array $prefs = null);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use Config\Database as DbConfig;
|
||||
|
||||
/**
|
||||
* @see \CodeIgniter\Database\ConfigTest
|
||||
*/
|
||||
class Config extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* Cache for instance of any connections that
|
||||
* have been requested as a "shared" instance.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* The main instance used to manage all of
|
||||
* our open database connections.
|
||||
*
|
||||
* @var Database|null
|
||||
*/
|
||||
protected static $factory;
|
||||
|
||||
/**
|
||||
* Returns the database connection
|
||||
*
|
||||
* @param array|BaseConnection|non-empty-string|null $group The name of the connection group to use,
|
||||
* or an array of configuration settings.
|
||||
* @param bool $getShared Whether to return a shared instance of the connection.
|
||||
*
|
||||
* @return BaseConnection
|
||||
*/
|
||||
public static function connect($group = null, bool $getShared = true)
|
||||
{
|
||||
// If a DB connection is passed in, just pass it back
|
||||
if ($group instanceof BaseConnection) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
if (is_array($group)) {
|
||||
$config = $group;
|
||||
$group = 'custom-' . md5(json_encode($config));
|
||||
} else {
|
||||
$dbConfig = config(DbConfig::class);
|
||||
|
||||
if ($group === null) {
|
||||
$group = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
|
||||
}
|
||||
|
||||
assert(is_string($group));
|
||||
|
||||
if (! isset($dbConfig->{$group})) {
|
||||
throw new InvalidArgumentException('"' . $group . '" is not a valid database connection group.');
|
||||
}
|
||||
|
||||
$config = $dbConfig->{$group};
|
||||
}
|
||||
|
||||
if ($getShared && isset(static::$instances[$group])) {
|
||||
return static::$instances[$group];
|
||||
}
|
||||
|
||||
static::ensureFactory();
|
||||
|
||||
$connection = static::$factory->load($config, $group);
|
||||
|
||||
static::$instances[$group] = $connection;
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all db connections currently made.
|
||||
*/
|
||||
public static function getConnections(): array
|
||||
{
|
||||
return static::$instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns an instance of the Forge for the specified
|
||||
* database group, and loads the group if it hasn't been loaded yet.
|
||||
*
|
||||
* @param array|ConnectionInterface|string|null $group
|
||||
*
|
||||
* @return Forge
|
||||
*/
|
||||
public static function forge($group = null)
|
||||
{
|
||||
$db = static::connect($group);
|
||||
|
||||
return static::$factory->loadForge($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the Database Utilities class.
|
||||
*
|
||||
* @param array|string|null $group
|
||||
*
|
||||
* @return BaseUtils
|
||||
*/
|
||||
public static function utils($group = null)
|
||||
{
|
||||
$db = static::connect($group);
|
||||
|
||||
return static::$factory->loadUtils($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the Database Seeder.
|
||||
*
|
||||
* @param non-empty-string|null $group
|
||||
*
|
||||
* @return Seeder
|
||||
*/
|
||||
public static function seeder(?string $group = null)
|
||||
{
|
||||
$config = config(DbConfig::class);
|
||||
|
||||
return new Seeder($config, static::connect($group));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the database Connection Manager/Factory is loaded and ready to use.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function ensureFactory()
|
||||
{
|
||||
if (static::$factory instanceof Database) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::$factory = new Database();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* @template TConnection
|
||||
* @template TResult
|
||||
*
|
||||
* @property false|object|resource $connID
|
||||
* @property-read string $DBDriver
|
||||
*/
|
||||
interface ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Initializes the database connection/settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize();
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return false|TConnection
|
||||
*/
|
||||
public function connect(bool $persistent = false);
|
||||
|
||||
/**
|
||||
* Create a persistent database connection.
|
||||
*
|
||||
* @return false|TConnection
|
||||
*/
|
||||
public function persistentConnect();
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reconnect();
|
||||
|
||||
/**
|
||||
* Returns the actual connection object. If both a 'read' and 'write'
|
||||
* connection has been specified, you can pass either term in to
|
||||
* get that connection. If you pass either alias in and only a single
|
||||
* connection is present, it must return the sole connection.
|
||||
*
|
||||
* @return false|TConnection
|
||||
*/
|
||||
public function getConnection(?string $alias = null);
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setDatabase(string $databaseName);
|
||||
|
||||
/**
|
||||
* Returns the name of the current database being used.
|
||||
*/
|
||||
public function getDatabase(): string;
|
||||
|
||||
/**
|
||||
* Returns the last error encountered by this connection.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array;
|
||||
|
||||
/**
|
||||
* The name of the platform in use (MySQLi, mssql, etc)
|
||||
*/
|
||||
public function getPlatform(): string;
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Orchestrates a query against the database. Queries must use
|
||||
* Database\Statement objects to store the query and build it.
|
||||
* This method works with the cache.
|
||||
*
|
||||
* Should automatically handle different connections for read/write
|
||||
* queries if needed.
|
||||
*
|
||||
* @param array|string|null $binds
|
||||
*
|
||||
* @return BaseResult<TConnection, TResult>|bool|Query
|
||||
*/
|
||||
public function query(string $sql, $binds = null);
|
||||
|
||||
/**
|
||||
* Performs a basic query against the database. No binding or caching
|
||||
* is performed, nor are transactions handled. Simply takes a raw
|
||||
* query string and returns the database-specific result id.
|
||||
*
|
||||
* @return false|TResult
|
||||
*/
|
||||
public function simpleQuery(string $sql);
|
||||
|
||||
/**
|
||||
* Returns an instance of the query builder for this connection.
|
||||
*
|
||||
* @param array|string $tableName Table name.
|
||||
*
|
||||
* @return BaseBuilder Builder.
|
||||
*/
|
||||
public function table($tableName);
|
||||
|
||||
/**
|
||||
* Returns the last query's statement object.
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function getLastQuery();
|
||||
|
||||
/**
|
||||
* "Smart" Escaping
|
||||
*
|
||||
* Escapes data based on type.
|
||||
* Sets boolean and null types.
|
||||
*
|
||||
* @param array|bool|float|int|object|string|null $str
|
||||
*
|
||||
* @return ($str is array ? array : float|int|string)
|
||||
*/
|
||||
public function escape($str);
|
||||
|
||||
/**
|
||||
* Allows for custom calls to the database engine that are not
|
||||
* supported through our database layer.
|
||||
*
|
||||
* @param array ...$params
|
||||
*
|
||||
* @return array|bool|float|int|object|resource|string|null
|
||||
*/
|
||||
public function callFunction(string $functionName, ...$params);
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*
|
||||
* @param string $sql
|
||||
*/
|
||||
public function isWriteType($sql): bool;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Database Connection Factory
|
||||
*
|
||||
* Creates and returns an instance of the appropriate Database Connection.
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
/**
|
||||
* Maintains an array of the instances of all connections that have
|
||||
* been created.
|
||||
*
|
||||
* Helps to keep track of all open connections for performance
|
||||
* monitoring, logging, etc.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $connections = [];
|
||||
|
||||
/**
|
||||
* Parses the connection binds and creates a Database Connection instance.
|
||||
*
|
||||
* @return BaseConnection
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function load(array $params = [], string $alias = '')
|
||||
{
|
||||
if ($alias === '') {
|
||||
throw new InvalidArgumentException('You must supply the parameter: alias.');
|
||||
}
|
||||
|
||||
if (! empty($params['DSN']) && str_contains($params['DSN'], '://')) {
|
||||
$params = $this->parseDSN($params);
|
||||
}
|
||||
|
||||
if (empty($params['DBDriver'])) {
|
||||
throw new InvalidArgumentException('You have not selected a database type to connect to.');
|
||||
}
|
||||
|
||||
assert($this->checkDbExtension($params['DBDriver']));
|
||||
|
||||
$this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params);
|
||||
|
||||
return $this->connections[$alias];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Forge instance for the current database type.
|
||||
*/
|
||||
public function loadForge(ConnectionInterface $db): Forge
|
||||
{
|
||||
if (! $db->connID) {
|
||||
$db->initialize();
|
||||
}
|
||||
|
||||
return $this->initDriver($db->DBDriver, 'Forge', $db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of Utils for the current database type.
|
||||
*/
|
||||
public function loadUtils(ConnectionInterface $db): BaseUtils
|
||||
{
|
||||
if (! $db->connID) {
|
||||
$db->initialize();
|
||||
}
|
||||
|
||||
return $this->initDriver($db->DBDriver, 'Utils', $db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses universal DSN string
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function parseDSN(array $params): array
|
||||
{
|
||||
$dsn = parse_url($params['DSN']);
|
||||
|
||||
if ($dsn === 0 || $dsn === '' || $dsn === '0' || $dsn === [] || $dsn === false || $dsn === null) {
|
||||
throw new InvalidArgumentException('Your DSN connection string is invalid.');
|
||||
}
|
||||
|
||||
$dsnParams = [
|
||||
'DSN' => '',
|
||||
'DBDriver' => $dsn['scheme'],
|
||||
'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
|
||||
'port' => isset($dsn['port']) ? rawurldecode((string) $dsn['port']) : '',
|
||||
'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
|
||||
'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
|
||||
'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '',
|
||||
];
|
||||
|
||||
if (isset($dsn['query']) && ($dsn['query'] !== '')) {
|
||||
parse_str($dsn['query'], $extra);
|
||||
|
||||
foreach ($extra as $key => $val) {
|
||||
if (is_string($val) && in_array(strtolower($val), ['true', 'false', 'null'], true)) {
|
||||
$val = $val === 'null' ? null : filter_var($val, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$dsnParams[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($params, $dsnParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database object.
|
||||
*
|
||||
* @param string $driver Driver name. FQCN can be used.
|
||||
* @param string $class 'Connection'|'Forge'|'Utils'
|
||||
* @param array|ConnectionInterface $argument The constructor parameter or DB connection
|
||||
*
|
||||
* @return BaseConnection|BaseUtils|Forge
|
||||
*/
|
||||
protected function initDriver(string $driver, string $class, $argument): object
|
||||
{
|
||||
$classname = (! str_contains($driver, '\\'))
|
||||
? "CodeIgniter\\Database\\{$driver}\\{$class}"
|
||||
: $driver . '\\' . $class;
|
||||
|
||||
return new $classname($argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the PHP database extension is loaded.
|
||||
*
|
||||
* @param string $driver DB driver or FQCN for custom driver
|
||||
*/
|
||||
private function checkDbExtension(string $driver): bool
|
||||
{
|
||||
if (str_contains($driver, '\\')) {
|
||||
// Cannot check a fully qualified classname for a custom driver.
|
||||
return true;
|
||||
}
|
||||
|
||||
$extensionMap = [
|
||||
// DBDriver => PHP extension
|
||||
'MySQLi' => 'mysqli',
|
||||
'SQLite3' => 'sqlite3',
|
||||
'Postgre' => 'pgsql',
|
||||
'SQLSRV' => 'sqlsrv',
|
||||
'OCI8' => 'oci8',
|
||||
];
|
||||
|
||||
$extension = $extensionMap[$driver] ?? '';
|
||||
|
||||
if ($extension === '') {
|
||||
$message = 'Invalid DBDriver name: "' . $driver . '"';
|
||||
|
||||
throw new ConfigException($message);
|
||||
}
|
||||
|
||||
if (extension_loaded($extension)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$message = 'The required PHP extension "' . $extension . '" is not loaded.'
|
||||
. ' Install and enable it to use "' . $driver . '" driver.';
|
||||
|
||||
throw new CriticalError($message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\DebugTraceableTrait;
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
|
||||
class DataException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
/**
|
||||
* Used by the Model's trigger() method when the callback cannot be found.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidMethodTriggered(string $method)
|
||||
{
|
||||
return new static(lang('Database.invalidEvent', [$method]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Model's insert/update methods when there isn't
|
||||
* any data to actually work with.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyDataset(string $mode)
|
||||
{
|
||||
return new static(lang('Database.emptyDataset', [$mode]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Model's insert/update methods when there is no
|
||||
* primary key defined and Model has option `useAutoIncrement`
|
||||
* set to false.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyPrimaryKey(string $mode)
|
||||
{
|
||||
return new static(lang('Database.emptyPrimaryKey', [$mode]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an argument for one of the Model's methods
|
||||
* were empty or otherwise invalid, and they could not be
|
||||
* to work correctly for that method.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidArgument(string $argument)
|
||||
{
|
||||
return new static(lang('Database.invalidArgument', [$argument]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidAllowedFields(string $model)
|
||||
{
|
||||
return new static(lang('Database.invalidAllowedFields', [$model]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forTableNotFound(string $table)
|
||||
{
|
||||
return new static(lang('Database.tableNotFound', [$table]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyInputGiven(string $argument)
|
||||
{
|
||||
return new static(lang('Database.forEmptyInputGiven', [$argument]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forFindColumnHaveMultipleColumns()
|
||||
{
|
||||
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\HasExitCodeInterface;
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
|
||||
class DatabaseException extends RuntimeException implements ExceptionInterface, HasExitCodeInterface
|
||||
{
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return EXIT_DATABASE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
/**
|
||||
* Provides a domain-level interface for broad capture
|
||||
* of all database-related exceptions.
|
||||
*
|
||||
* catch (\CodeIgniter\Database\Exceptions\ExceptionInterface) { ... }
|
||||
*/
|
||||
interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface
|
||||
{
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* Class Migration
|
||||
*/
|
||||
abstract class Migration
|
||||
{
|
||||
/**
|
||||
* The name of the database group to use.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $DBGroup;
|
||||
|
||||
/**
|
||||
* Database Connection instance
|
||||
*
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Database Forge instance.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
public function __construct(?Forge $forge = null)
|
||||
{
|
||||
if (isset($this->DBGroup)) {
|
||||
$this->forge = Database::forge($this->DBGroup);
|
||||
} elseif ($forge instanceof Forge) {
|
||||
$this->forge = $forge;
|
||||
} else {
|
||||
$this->forge = Database::forge(config(Database::class)->defaultGroup);
|
||||
}
|
||||
|
||||
$this->db = $this->forge->getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database group name this migration uses.
|
||||
*/
|
||||
public function getDBGroup(): ?string
|
||||
{
|
||||
return $this->DBGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a migration step.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function up();
|
||||
|
||||
/**
|
||||
* Revert a migration step.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function down();
|
||||
}
|
||||
@@ -0,0 +1,881 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Database;
|
||||
use Config\Migrations as MigrationsConfig;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class MigrationRunner
|
||||
*/
|
||||
class MigrationRunner
|
||||
{
|
||||
/**
|
||||
* Whether or not migrations are allowed to run.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = false;
|
||||
|
||||
/**
|
||||
* Name of table to store meta information
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* The Namespace where migrations can be found.
|
||||
* `null` is all namespaces.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $namespace;
|
||||
|
||||
/**
|
||||
* The database Group to migrate.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $group;
|
||||
|
||||
/**
|
||||
* The migration name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The pattern used to locate migration file versions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $regex = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/';
|
||||
|
||||
/**
|
||||
* The main database connection. Used to store
|
||||
* migration information in.
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* If true, will continue instead of throwing
|
||||
* exceptions.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* used to return messages for CLI.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cliMessages = [];
|
||||
|
||||
/**
|
||||
* Tracks whether we have already ensured
|
||||
* the table exists or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $tableChecked = false;
|
||||
|
||||
/**
|
||||
* The full path to locate migration files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The database Group filter.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $groupFilter;
|
||||
|
||||
/**
|
||||
* Used to skip current migration.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $groupSkip = false;
|
||||
|
||||
/**
|
||||
* The migration can manage multiple databases. So it should always use the
|
||||
* default DB group so that it creates the `migrations` table in the default
|
||||
* DB group. Therefore, passing $db is for testing purposes only.
|
||||
*
|
||||
* @param array|ConnectionInterface|string|null $db DB group. For testing purposes only.
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function __construct(MigrationsConfig $config, $db = null)
|
||||
{
|
||||
$this->enabled = $config->enabled ?? false;
|
||||
$this->table = $config->table ?? 'migrations';
|
||||
|
||||
$this->namespace = APP_NAMESPACE;
|
||||
|
||||
// Even if a DB connection is passed, since it is a test,
|
||||
// it is assumed to use the default group name
|
||||
$this->group = is_string($db) ? $db : config(Database::class)->defaultGroup;
|
||||
|
||||
$this->db = db_connect($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate and run all new migrations
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function latest(?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
if ($group !== null) {
|
||||
$this->groupFilter = $group;
|
||||
$this->setGroup($group);
|
||||
}
|
||||
|
||||
$migrations = $this->findMigrations();
|
||||
|
||||
if ($migrations === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->getHistory((string) $group) as $history) {
|
||||
unset($migrations[$this->getObjectUid($history)]);
|
||||
}
|
||||
|
||||
$batch = $this->getLastBatch() + 1;
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
if ($this->migrate('up', $migration)) {
|
||||
if ($this->groupSkip === true) {
|
||||
$this->groupSkip = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addHistory($migration, $batch);
|
||||
} else {
|
||||
$this->regress(-1);
|
||||
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$data = get_object_vars($this);
|
||||
$data['method'] = 'latest';
|
||||
Events::trigger('migrate', $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate down to a previous batch
|
||||
*
|
||||
* Calls each migration step required to get to the provided batch
|
||||
*
|
||||
* @param int $targetBatch Target batch number, or negative for a relative batch, 0 for all
|
||||
* @param string|null $group Deprecated. The designation has no effect.
|
||||
*
|
||||
* @return bool True on success, FALSE on failure or no migrations are found
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function regress(int $targetBatch = 0, ?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
$batches = $this->getBatches();
|
||||
|
||||
if ($targetBatch < 0) {
|
||||
$targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
|
||||
}
|
||||
|
||||
if ($batches === [] && $targetBatch === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
|
||||
$message = lang('Migrations.batchNotFound') . $targetBatch;
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$tmpNamespace = $this->namespace;
|
||||
|
||||
$this->namespace = null;
|
||||
$allMigrations = $this->findMigrations();
|
||||
|
||||
$migrations = [];
|
||||
|
||||
while ($batch = array_pop($batches)) {
|
||||
if ($batch <= $targetBatch) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($this->getBatchHistory($batch, 'desc') as $history) {
|
||||
$uid = $this->getObjectUid($history);
|
||||
|
||||
if (! isset($allMigrations[$uid])) {
|
||||
$message = lang('Migrations.gap') . ' ' . $history->version;
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$migration = $allMigrations[$uid];
|
||||
$migration->history = $history;
|
||||
$migrations[] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
if ($this->migrate('down', $migration)) {
|
||||
$this->removeHistory($migration->history);
|
||||
} else {
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$data = get_object_vars($this);
|
||||
$data['method'] = 'regress';
|
||||
Events::trigger('migrate', $data);
|
||||
|
||||
$this->namespace = $tmpNamespace;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a single file regardless of order or batches.
|
||||
* Method "up" or "down" determined by presence in history.
|
||||
* NOTE: This is not recommended and provided mostly for testing.
|
||||
*
|
||||
* @param string $path Full path to a valid migration file
|
||||
* @param string $path Namespace of the target migration
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function force(string $path, string $namespace, ?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
if ($group !== null) {
|
||||
$this->groupFilter = $group;
|
||||
$this->setGroup($group);
|
||||
}
|
||||
|
||||
$migration = $this->migrationFromFile($path, $namespace);
|
||||
if (empty($migration)) {
|
||||
$message = lang('Migrations.notFound');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$method = 'up';
|
||||
$this->setNamespace($migration->namespace);
|
||||
|
||||
foreach ($this->getHistory($this->group) as $history) {
|
||||
if ($this->getObjectUid($history) === $migration->uid) {
|
||||
$method = 'down';
|
||||
$migration->history = $history;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($method === 'up') {
|
||||
$batch = $this->getLastBatch() + 1;
|
||||
|
||||
if ($this->migrate('up', $migration) && $this->groupSkip === false) {
|
||||
$this->addHistory($migration, $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->groupSkip = false;
|
||||
} elseif ($this->migrate('down', $migration)) {
|
||||
$this->removeHistory($migration->history);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves list of available migration scripts
|
||||
*
|
||||
* @return array List of all located migrations by their UID
|
||||
*/
|
||||
public function findMigrations(): array
|
||||
{
|
||||
$namespaces = $this->namespace !== null ? [$this->namespace] : array_keys(service('autoloader')->getNamespace());
|
||||
$migrations = [];
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
if (ENVIRONMENT !== 'testing' && $namespace === 'Tests\Support') {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->findNamespaceMigrations($namespace) as $migration) {
|
||||
$migrations[$migration->uid] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort migrations ascending by their UID (version)
|
||||
ksort($migrations);
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of available migration scripts for one namespace
|
||||
*/
|
||||
public function findNamespaceMigrations(string $namespace): array
|
||||
{
|
||||
$migrations = [];
|
||||
$locator = service('locator', true);
|
||||
|
||||
if (! empty($this->path)) {
|
||||
helper('filesystem');
|
||||
$dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
|
||||
$files = get_filenames($dir, true, false, false);
|
||||
} else {
|
||||
$files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file);
|
||||
|
||||
if ($migration = $this->migrationFromFile($file, $namespace)) {
|
||||
$migrations[] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a migration object from a file path.
|
||||
*
|
||||
* @param string $path Full path to a valid migration file.
|
||||
*
|
||||
* @return false|object Returns the migration object, or false on failure
|
||||
*/
|
||||
protected function migrationFromFile(string $path, string $namespace)
|
||||
{
|
||||
if (! str_ends_with($path, '.php')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filename = basename($path, '.php');
|
||||
|
||||
if (preg_match($this->regex, $filename) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$locator = service('locator', true);
|
||||
|
||||
$migration = new stdClass();
|
||||
|
||||
$migration->version = $this->getMigrationNumber($filename);
|
||||
$migration->name = $this->getMigrationName($filename);
|
||||
$migration->path = $path;
|
||||
$migration->class = $locator->getClassname($path);
|
||||
$migration->namespace = $namespace;
|
||||
$migration->uid = $this->getObjectUid($migration);
|
||||
|
||||
return $migration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows other scripts to modify on the fly as needed.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setNamespace(?string $namespace)
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows other scripts to modify on the fly as needed.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setGroup(string $group)
|
||||
{
|
||||
$this->group = $group;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If $silent == true, then will not throw exceptions and will
|
||||
* attempt to continue gracefully.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setSilent(bool $silent)
|
||||
{
|
||||
$this->silent = $silent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the migration number from a filename
|
||||
*
|
||||
* @param string $migration A migration filename w/o path.
|
||||
*/
|
||||
protected function getMigrationNumber(string $migration): string
|
||||
{
|
||||
preg_match($this->regex, $migration, $matches);
|
||||
|
||||
return $matches !== [] ? $matches[1] : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the migration name from a filename
|
||||
*
|
||||
* Note: The migration name should be the classname, but maybe they are
|
||||
* different.
|
||||
*
|
||||
* @param string $migration A migration filename w/o path.
|
||||
*/
|
||||
protected function getMigrationName(string $migration): string
|
||||
{
|
||||
preg_match($this->regex, $migration, $matches);
|
||||
|
||||
return $matches !== [] ? $matches[2] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the non-repeatable portions of a migration or history
|
||||
* to create a sortable unique key
|
||||
*
|
||||
* @param object $object migration or $history
|
||||
*/
|
||||
public function getObjectUid($object): string
|
||||
{
|
||||
return preg_replace('/[^0-9]/', '', $object->version) . $object->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves messages formatted for CLI output
|
||||
*/
|
||||
public function getCliMessages(): array
|
||||
{
|
||||
return $this->cliMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any CLI messages.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function clearCliMessages()
|
||||
{
|
||||
$this->cliMessages = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the history table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearHistory()
|
||||
{
|
||||
if ($this->db->tableExists($this->table)) {
|
||||
$this->db->table($this->table)->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a history to the table.
|
||||
*
|
||||
* @param object $migration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addHistory($migration, int $batch)
|
||||
{
|
||||
$this->db->table($this->table)->insert([
|
||||
'version' => $migration->version,
|
||||
'class' => $migration->class,
|
||||
'group' => $this->group,
|
||||
'namespace' => $migration->namespace,
|
||||
'time' => Time::now()->getTimestamp(),
|
||||
'batch' => $batch,
|
||||
]);
|
||||
|
||||
if (is_cli()) {
|
||||
$this->cliMessages[] = sprintf(
|
||||
"\t%s(%s) %s_%s",
|
||||
CLI::color(lang('Migrations.added'), 'yellow'),
|
||||
$migration->namespace,
|
||||
$migration->version,
|
||||
$migration->class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single history
|
||||
*
|
||||
* @param object $history
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function removeHistory($history)
|
||||
{
|
||||
$this->db->table($this->table)->where('id', $history->id)->delete();
|
||||
|
||||
if (is_cli()) {
|
||||
$this->cliMessages[] = sprintf(
|
||||
"\t%s(%s) %s_%s",
|
||||
CLI::color(lang('Migrations.removed'), 'yellow'),
|
||||
$history->namespace,
|
||||
$history->version,
|
||||
$history->class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the full migration history from the database for a group
|
||||
*/
|
||||
public function getHistory(string $group = 'default'): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$builder = $this->db->table($this->table);
|
||||
|
||||
// If group was specified then use it
|
||||
if ($group !== '') {
|
||||
$builder->where('group', $group);
|
||||
}
|
||||
|
||||
// If a namespace was specified then use it
|
||||
if ($this->namespace !== null) {
|
||||
$builder->where('namespace', $this->namespace);
|
||||
}
|
||||
|
||||
$query = $builder->orderBy('id', 'ASC')->get();
|
||||
|
||||
return ! empty($query) ? $query->getResultObject() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the migration history for a single batch.
|
||||
*
|
||||
* @param string $order
|
||||
*/
|
||||
public function getBatchHistory(int $batch, $order = 'asc'): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$query = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', $order)
|
||||
->get();
|
||||
|
||||
return ! empty($query) ? $query->getResultObject() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the batches from the database history in order
|
||||
*/
|
||||
public function getBatches(): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$batches = $this->db->table($this->table)
|
||||
->select('batch')
|
||||
->distinct()
|
||||
->orderBy('batch', 'asc')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return array_map('intval', array_column($batches, 'batch'));
|
||||
//return array_map(intval(...), array_column($batches, 'batch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the last batch in the database.
|
||||
*/
|
||||
public function getLastBatch(): int
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$batch = $this->db->table($this->table)
|
||||
->selectMax('batch')
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
$batch = is_array($batch) && $batch !== []
|
||||
? end($batch)->batch
|
||||
: 0;
|
||||
|
||||
return (int) $batch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number of the first migration for a batch.
|
||||
* Mostly just for tests.
|
||||
*/
|
||||
public function getBatchStart(int $batch): string
|
||||
{
|
||||
if ($batch < 0) {
|
||||
$batches = $this->getBatches();
|
||||
$batch = $batches[count($batches) - 1] ?? 0;
|
||||
}
|
||||
|
||||
$migration = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', 'asc')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
return $migration !== [] ? $migration[0]->version : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number of the last migration for a batch.
|
||||
* Mostly just for tests.
|
||||
*/
|
||||
public function getBatchEnd(int $batch): string
|
||||
{
|
||||
if ($batch < 0) {
|
||||
$batches = $this->getBatches();
|
||||
$batch = $batches[count($batches) - 1] ?? 0;
|
||||
}
|
||||
|
||||
$migration = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', 'desc')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
return $migration === [] ? '0' : $migration[0]->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that we have created our migrations table
|
||||
* in the database.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ensureTable()
|
||||
{
|
||||
if ($this->tableChecked || $this->db->tableExists($this->table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$forge = Database::forge($this->db);
|
||||
|
||||
$forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'version' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'class' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'group' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'namespace' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'time' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => false,
|
||||
],
|
||||
'batch' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$forge->addPrimaryKey('id');
|
||||
$forge->createTable($this->table, true);
|
||||
|
||||
$this->tableChecked = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the actual running of a migration.
|
||||
*
|
||||
* @param string $direction "up" or "down"
|
||||
* @param object $migration The migration to run
|
||||
*/
|
||||
protected function migrate($direction, $migration): bool
|
||||
{
|
||||
include_once $migration->path;
|
||||
|
||||
$class = $migration->class;
|
||||
$this->setName($migration->name);
|
||||
|
||||
// Validate the migration file structure
|
||||
if (! class_exists($class, false)) {
|
||||
$message = sprintf(lang('Migrations.classNotFound'), $class);
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
/** @var Migration $instance */
|
||||
$instance = new $class(Database::forge($this->db));
|
||||
$group = $instance->getDBGroup() ?? $this->group;
|
||||
|
||||
if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->groupSkip = true;
|
||||
|
||||
return true;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
|
||||
$this->groupSkip = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! is_callable([$instance, $direction])) {
|
||||
$message = sprintf(lang('Migrations.missingMethod'), $direction);
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$instance->{$direction}();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
|
||||
/**
|
||||
* Builder for MySQLi
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $escapeChar = '`';
|
||||
|
||||
/**
|
||||
* Specifies which sql statements
|
||||
* support the ignore option.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'update' => 'IGNORE',
|
||||
'insert' => 'IGNORE',
|
||||
'delete' => 'IGNORE',
|
||||
];
|
||||
|
||||
/**
|
||||
* FROM tables
|
||||
*
|
||||
* Groups tables in FROM clauses if needed, so there is no confusion
|
||||
* about operator precedence.
|
||||
*
|
||||
* Note: This is only used (and overridden) by MySQL.
|
||||
*/
|
||||
protected function _fromTables(): string
|
||||
{
|
||||
if ($this->QBJoin !== [] && count($this->QBFrom) > 1) {
|
||||
return '(' . implode(', ', $this->QBFrom) . ')';
|
||||
}
|
||||
|
||||
return implode(', ', $this->QBFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$updateFields = $this->QBOptions['updateFields'] ??
|
||||
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
|
||||
[];
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '`_u`';
|
||||
|
||||
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
|
||||
|
||||
$sql .= "INNER JOIN (\n{:_table_:}";
|
||||
|
||||
$sql .= ') ' . $alias . "\n";
|
||||
|
||||
$sql .= 'ON ' . implode(
|
||||
' AND ',
|
||||
array_map(
|
||||
static fn ($key, $value) => (
|
||||
($value instanceof RawSql && is_string($key))
|
||||
?
|
||||
$table . '.' . $key . ' = ' . $value
|
||||
:
|
||||
(
|
||||
$value instanceof RawSql
|
||||
?
|
||||
$value
|
||||
:
|
||||
$table . '.' . $value . ' = ' . $alias . '.' . $value
|
||||
)
|
||||
),
|
||||
array_keys($constraints),
|
||||
$constraints,
|
||||
),
|
||||
) . "\n";
|
||||
|
||||
$sql .= "SET\n";
|
||||
|
||||
$sql .= implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($key, $value): string => $table . '.' . $key . ($value instanceof RawSql ?
|
||||
' = ' . $value :
|
||||
' = ' . $alias . '.' . $value),
|
||||
array_keys($updateFields),
|
||||
$updateFields,
|
||||
),
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = implode(
|
||||
" UNION ALL\n",
|
||||
array_map(
|
||||
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for MySQLi
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* CREATE DATABASE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
|
||||
|
||||
/**
|
||||
* CREATE DATABASE IF statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseIfStr = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET %s COLLATE %s';
|
||||
|
||||
/**
|
||||
* DROP CONSTRAINT statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s';
|
||||
|
||||
/**
|
||||
* CREATE TABLE keys flag
|
||||
*
|
||||
* Whether table keys are created from within the
|
||||
* CREATE TABLE statement.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $createTableKeys = true;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_unsigned = [
|
||||
'TINYINT',
|
||||
'SMALLINT',
|
||||
'MEDIUMINT',
|
||||
'INT',
|
||||
'INTEGER',
|
||||
'BIGINT',
|
||||
'REAL',
|
||||
'DOUBLE',
|
||||
'DOUBLE PRECISION',
|
||||
'FLOAT',
|
||||
'DECIMAL',
|
||||
'NUMERIC',
|
||||
];
|
||||
|
||||
/**
|
||||
* Table Options list which required to be quoted
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_quoted_table_options = [
|
||||
'COMMENT',
|
||||
'COMPRESSION',
|
||||
'CONNECTION',
|
||||
'DATA DIRECTORY',
|
||||
'INDEX DIRECTORY',
|
||||
'ENCRYPTION',
|
||||
'PASSWORD',
|
||||
];
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* CREATE TABLE attributes
|
||||
*
|
||||
* @param array $attributes Associative array of table attributes
|
||||
*/
|
||||
protected function _createTableAttributes(array $attributes): string
|
||||
{
|
||||
$sql = '';
|
||||
|
||||
foreach (array_keys($attributes) as $key) {
|
||||
if (is_string($key)) {
|
||||
$sql .= ' ' . strtoupper($key) . ' = ';
|
||||
|
||||
if (in_array(strtoupper($key), $this->_quoted_table_options, true)) {
|
||||
$sql .= $this->db->escape($attributes[$key]);
|
||||
} else {
|
||||
$sql .= $this->db->escapeString($attributes[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->db->charset !== '' && ! str_contains($sql, 'CHARACTER SET') && ! str_contains($sql, 'CHARSET')) {
|
||||
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
|
||||
}
|
||||
|
||||
if ($this->db->DBCollat !== '' && ! str_contains($sql, 'COLLATE')) {
|
||||
$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @param string $alterType ALTER type
|
||||
* @param string $table Table name
|
||||
* @param array|string $processedFields Processed column definitions
|
||||
* or column names to DROP
|
||||
*
|
||||
* @return ($alterType is 'DROP' ? string : list<string>)
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $processedFields)
|
||||
{
|
||||
if ($alterType === 'DROP') {
|
||||
return parent::_alterTable($alterType, $table, $processedFields);
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
|
||||
|
||||
foreach ($processedFields as $i => $field) {
|
||||
if ($field['_literal'] !== false) {
|
||||
$processedFields[$i] = ($alterType === 'ADD') ? "\n\tADD " . $field['_literal'] : "\n\tMODIFY " . $field['_literal'];
|
||||
} else {
|
||||
if ($alterType === 'ADD') {
|
||||
$processedFields[$i]['_literal'] = "\n\tADD ";
|
||||
} else {
|
||||
$processedFields[$i]['_literal'] = empty($field['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
|
||||
}
|
||||
|
||||
$processedFields[$i] = $processedFields[$i]['_literal'] . $this->_processColumn($processedFields[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
return [$sql . implode(',', $processedFields)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $processedField): string
|
||||
{
|
||||
$extraClause = isset($processedField['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($processedField['after']) : '';
|
||||
|
||||
if (empty($extraClause) && isset($processedField['first']) && $processedField['first'] === true) {
|
||||
$extraClause = ' FIRST';
|
||||
}
|
||||
|
||||
return $this->db->escapeIdentifiers($processedField['name'])
|
||||
. (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name']))
|
||||
. ' ' . $processedField['type'] . $processedField['length']
|
||||
. $processedField['unsigned']
|
||||
. $processedField['null']
|
||||
. $processedField['default']
|
||||
. $processedField['auto_increment']
|
||||
. $processedField['unique']
|
||||
. (empty($processedField['comment']) ? '' : ' COMMENT ' . $processedField['comment'])
|
||||
. $extraClause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL to add indexes
|
||||
*
|
||||
* @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
|
||||
*/
|
||||
protected function _processIndexes(string $table, bool $asQuery = false): array
|
||||
{
|
||||
$sqls = [''];
|
||||
$index = 0;
|
||||
|
||||
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
|
||||
$index = $i;
|
||||
if ($asQuery === false) {
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
if (isset($this->keys[$i]['fields'])) {
|
||||
for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
|
||||
if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
|
||||
unset($this->keys[$i]['fields'][$i2]);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_array($this->keys[$i]['fields'])) {
|
||||
$this->keys[$i]['fields'] = [$this->keys[$i]['fields']];
|
||||
}
|
||||
|
||||
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
|
||||
|
||||
$keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
|
||||
implode('_', $this->keys[$i]['fields']) :
|
||||
$this->keys[$i]['keyName']);
|
||||
|
||||
if ($asQuery) {
|
||||
$sqls[$index] = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . " ADD {$unique}KEY "
|
||||
. $keyName
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
|
||||
} else {
|
||||
$sqls[$index] .= ",\n\t{$unique}KEY " . $keyName
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$this->keys = [];
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Key
|
||||
*/
|
||||
public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool
|
||||
{
|
||||
$sql = sprintf(
|
||||
$this->dropIndexStr,
|
||||
$this->db->escapeIdentifiers($keyName),
|
||||
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
|
||||
);
|
||||
|
||||
return $this->db->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Primary Key
|
||||
*/
|
||||
public function dropPrimaryKey(string $table, string $keyName = ''): bool
|
||||
{
|
||||
$sql = sprintf(
|
||||
'ALTER TABLE %s DROP PRIMARY KEY',
|
||||
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
|
||||
);
|
||||
|
||||
return $this->db->query($sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use mysqli;
|
||||
use mysqli_result;
|
||||
use mysqli_sql_exception;
|
||||
use mysqli_stmt;
|
||||
|
||||
/**
|
||||
* Prepared query for MySQLi
|
||||
*
|
||||
* @extends BasePreparedQuery<mysqli, mysqli_stmt, mysqli_result>
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = []): PreparedQuery
|
||||
{
|
||||
// Mysqli driver doesn't like statements
|
||||
// with terminating semicolons.
|
||||
$sql = rtrim($sql, ';');
|
||||
|
||||
if (! $this->statement = $this->db->mysqli->prepare($sql)) {
|
||||
$this->errorCode = $this->db->mysqli->errno;
|
||||
$this->errorString = $this->db->mysqli->error;
|
||||
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
// First off - bind the parameters
|
||||
$bindTypes = '';
|
||||
$binaryData = [];
|
||||
|
||||
// Determine the type string
|
||||
foreach ($data as $key => $item) {
|
||||
if (is_int($item)) {
|
||||
$bindTypes .= 'i';
|
||||
} elseif (is_numeric($item)) {
|
||||
$bindTypes .= 'd';
|
||||
} elseif (is_string($item) && $this->isBinary($item)) {
|
||||
$bindTypes .= 'b';
|
||||
$binaryData[$key] = $item;
|
||||
} else {
|
||||
$bindTypes .= 's';
|
||||
}
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bind_param($bindTypes, ...$data);
|
||||
|
||||
// Stream binary data
|
||||
foreach ($binaryData as $key => $value) {
|
||||
$this->statement->send_long_data($key, $value);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->statement->execute();
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query or false on failure.
|
||||
*
|
||||
* @return false|mysqli_result
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->statement->get_result();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate prepared statements.
|
||||
*/
|
||||
protected function _close(): bool
|
||||
{
|
||||
return $this->statement->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use mysqli;
|
||||
use mysqli_result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for MySQLi
|
||||
*
|
||||
* @extends BaseResult<mysqli, mysqli_result>
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return $this->resultID->field_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
$this->resultID->field_seek(0);
|
||||
|
||||
while ($field = $this->resultID->fetch_field()) {
|
||||
$fieldNames[] = $field->name;
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
MYSQLI_TYPE_DECIMAL => 'decimal',
|
||||
MYSQLI_TYPE_NEWDECIMAL => 'newdecimal',
|
||||
MYSQLI_TYPE_FLOAT => 'float',
|
||||
MYSQLI_TYPE_DOUBLE => 'double',
|
||||
|
||||
MYSQLI_TYPE_BIT => 'bit',
|
||||
MYSQLI_TYPE_SHORT => 'short',
|
||||
MYSQLI_TYPE_LONG => 'long',
|
||||
MYSQLI_TYPE_LONGLONG => 'longlong',
|
||||
MYSQLI_TYPE_INT24 => 'int24',
|
||||
|
||||
MYSQLI_TYPE_YEAR => 'year',
|
||||
|
||||
MYSQLI_TYPE_TIMESTAMP => 'timestamp',
|
||||
MYSQLI_TYPE_DATE => 'date',
|
||||
MYSQLI_TYPE_TIME => 'time',
|
||||
MYSQLI_TYPE_DATETIME => 'datetime',
|
||||
MYSQLI_TYPE_NEWDATE => 'newdate',
|
||||
|
||||
MYSQLI_TYPE_SET => 'set',
|
||||
|
||||
MYSQLI_TYPE_VAR_STRING => 'var_string',
|
||||
MYSQLI_TYPE_STRING => 'string',
|
||||
|
||||
MYSQLI_TYPE_GEOMETRY => 'geometry',
|
||||
MYSQLI_TYPE_TINY_BLOB => 'tiny_blob',
|
||||
MYSQLI_TYPE_MEDIUM_BLOB => 'medium_blob',
|
||||
MYSQLI_TYPE_LONG_BLOB => 'long_blob',
|
||||
MYSQLI_TYPE_BLOB => 'blob',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
$fieldData = $this->resultID->fetch_fields();
|
||||
|
||||
foreach ($fieldData as $i => $data) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $data->name;
|
||||
$retVal[$i]->type = $data->type;
|
||||
$retVal[$i]->type_name = in_array($data->type, [1, 247], true) ? 'char' : ($dataTypes[$data->type] ?? null);
|
||||
$retVal[$i]->max_length = $data->max_length;
|
||||
$retVal[$i]->primary_key = $data->flags & 2;
|
||||
$retVal[$i]->length = $data->length;
|
||||
$retVal[$i]->default = $data->def;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_object($this->resultID)) {
|
||||
$this->resultID->free();
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
return $this->resultID->data_seek($n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return array|false|null
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return $this->resultID->fetch_assoc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return Entity|false|object|stdClass
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data);
|
||||
}
|
||||
|
||||
return $this->resultID->fetch_object($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the resultID (i.e., mysqli_result object)
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (! is_int($this->numRows)) {
|
||||
$this->numRows = $this->resultID->num_rows;
|
||||
}
|
||||
|
||||
return $this->numRows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for MySQLi
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $listDatabases = 'SHOW DATABASES';
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'OPTIMIZE TABLE %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
|
||||
/**
|
||||
* @template TConnection
|
||||
* @template TStatement
|
||||
* @template TResult
|
||||
*/
|
||||
interface PreparedQueryInterface
|
||||
{
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*
|
||||
* @return bool|ResultInterface<TConnection, TResult>
|
||||
*/
|
||||
public function execute(...$data);
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prepare(string $sql, array $options = []);
|
||||
|
||||
/**
|
||||
* Explicity closes the statement.
|
||||
*
|
||||
* @throws BadMethodCallException
|
||||
*/
|
||||
public function close(): bool;
|
||||
|
||||
/**
|
||||
* Returns the SQL that has been prepared.
|
||||
*/
|
||||
public function getQueryString(): string;
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int;
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string;
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Query builder
|
||||
*/
|
||||
class Query implements QueryInterface, Stringable
|
||||
{
|
||||
/**
|
||||
* The query string, as provided by the user.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $originalQueryString;
|
||||
|
||||
/**
|
||||
* The query string if table prefix has been swapped.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $swappedQueryString;
|
||||
|
||||
/**
|
||||
* The final query string after binding, etc.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $finalQueryString;
|
||||
|
||||
/**
|
||||
* The binds and their values used for binding.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $binds = [];
|
||||
|
||||
/**
|
||||
* Bind marker
|
||||
*
|
||||
* Character used to identify values in a prepared statement.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bindMarker = '?';
|
||||
|
||||
/**
|
||||
* The start time in seconds with microseconds
|
||||
* for when this query was executed.
|
||||
*
|
||||
* @var float|string
|
||||
*/
|
||||
protected $startTime;
|
||||
|
||||
/**
|
||||
* The end time in seconds with microseconds
|
||||
* for when this query was executed.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $endTime;
|
||||
|
||||
/**
|
||||
* The error code, if any.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $errorCode;
|
||||
|
||||
/**
|
||||
* The error message, if any.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $errorString;
|
||||
|
||||
/**
|
||||
* Pointer to database connection.
|
||||
* Mainly for escaping features.
|
||||
*
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
public $db;
|
||||
|
||||
public function __construct(ConnectionInterface $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the raw query string to use for this statement.
|
||||
*
|
||||
* @param mixed $binds
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
|
||||
{
|
||||
$this->originalQueryString = $sql;
|
||||
unset($this->swappedQueryString);
|
||||
|
||||
if ($binds !== null) {
|
||||
if (! is_array($binds)) {
|
||||
$binds = [$binds];
|
||||
}
|
||||
|
||||
if ($setEscape) {
|
||||
array_walk($binds, static function (&$item): void {
|
||||
$item = [
|
||||
$item,
|
||||
true,
|
||||
];
|
||||
});
|
||||
}
|
||||
$this->binds = $binds;
|
||||
}
|
||||
|
||||
unset($this->finalQueryString);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will store the variables to bind into the query later.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBinds(array $binds, bool $setEscape = true)
|
||||
{
|
||||
if ($setEscape) {
|
||||
array_walk($binds, static function (&$item): void {
|
||||
$item = [$item, true];
|
||||
});
|
||||
}
|
||||
|
||||
$this->binds = $binds;
|
||||
|
||||
unset($this->finalQueryString);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final, processed query string after binding, etal
|
||||
* has been performed.
|
||||
*/
|
||||
public function getQuery(): string
|
||||
{
|
||||
if (empty($this->finalQueryString)) {
|
||||
$this->compileBinds();
|
||||
}
|
||||
|
||||
return $this->finalQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the execution time of the statement using microtime(true)
|
||||
* for it's start and end values. If no end value is present, will
|
||||
* use the current time to determine total duration.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDuration(float $start, ?float $end = null)
|
||||
{
|
||||
$this->startTime = $start;
|
||||
|
||||
if ($end === null) {
|
||||
$end = microtime(true);
|
||||
}
|
||||
|
||||
$this->endTime = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start time in seconds with microseconds.
|
||||
*
|
||||
* @return float|string
|
||||
*/
|
||||
public function getStartTime(bool $returnRaw = false, int $decimals = 6)
|
||||
{
|
||||
if ($returnRaw) {
|
||||
return $this->startTime;
|
||||
}
|
||||
|
||||
return number_format($this->startTime, $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of this query during execution, or null if
|
||||
* the query has not been executed yet.
|
||||
*
|
||||
* @param int $decimals The accuracy of the returned time.
|
||||
*/
|
||||
public function getDuration(int $decimals = 6): string
|
||||
{
|
||||
return number_format(($this->endTime - $this->startTime), $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the error description that happened for this query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setError(int $code, string $error)
|
||||
{
|
||||
$this->errorCode = $code;
|
||||
$this->errorString = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this statement created an error not.
|
||||
*/
|
||||
public function hasError(): bool
|
||||
{
|
||||
return ! empty($this->errorString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return $this->errorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*/
|
||||
public function isWriteType(): bool
|
||||
{
|
||||
return $this->db->isWriteType($this->originalQueryString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps out one table prefix for a new one.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function swapPrefix(string $orig, string $swap)
|
||||
{
|
||||
$sql = $this->swappedQueryString ?? $this->originalQueryString;
|
||||
|
||||
$from = '/(\W)' . $orig . '(\S)/';
|
||||
$to = '\\1' . $swap . '\\2';
|
||||
|
||||
$this->swappedQueryString = preg_replace($from, $to, $sql);
|
||||
|
||||
unset($this->finalQueryString);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original SQL that was passed into the system.
|
||||
*/
|
||||
public function getOriginalQuery(): string
|
||||
{
|
||||
return $this->originalQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and inserts any binds into the finalQueryString property.
|
||||
*
|
||||
* @see https://regex101.com/r/EUEhay/5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function compileBinds()
|
||||
{
|
||||
$sql = $this->swappedQueryString ?? $this->originalQueryString;
|
||||
$binds = $this->binds;
|
||||
|
||||
if (empty($binds)) {
|
||||
$this->finalQueryString = $sql;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_int(array_key_first($binds))) {
|
||||
$bindCount = count($binds);
|
||||
$ml = strlen($this->bindMarker);
|
||||
|
||||
$this->finalQueryString = $this->matchSimpleBinds($sql, $binds, $bindCount, $ml);
|
||||
} else {
|
||||
// Reverse the binds so that duplicate named binds
|
||||
// will be processed prior to the original binds.
|
||||
$binds = array_reverse($binds);
|
||||
|
||||
$this->finalQueryString = $this->matchNamedBinds($sql, $binds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match bindings
|
||||
*/
|
||||
protected function matchNamedBinds(string $sql, array $binds): string
|
||||
{
|
||||
$replacers = [];
|
||||
|
||||
foreach ($binds as $placeholder => $value) {
|
||||
// $value[1] contains the boolean whether should be escaped or not
|
||||
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
|
||||
|
||||
// In order to correctly handle backlashes in saved strings
|
||||
// we will need to preg_quote, so remove the wrapping escape characters
|
||||
// otherwise it will get escaped.
|
||||
if (is_array($value[0])) {
|
||||
$escapedValue = '(' . implode(',', $escapedValue) . ')';
|
||||
}
|
||||
|
||||
$replacers[":{$placeholder}:"] = $escapedValue;
|
||||
}
|
||||
|
||||
return strtr($sql, $replacers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match bindings
|
||||
*/
|
||||
protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string
|
||||
{
|
||||
if ($c = preg_match_all("/'[^']*'/", $sql, $matches) >= 1) {
|
||||
$c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
// Bind values' count must match the count of markers in the query
|
||||
if ($bindCount !== $c) {
|
||||
return $sql;
|
||||
}
|
||||
} elseif (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
do {
|
||||
$c--;
|
||||
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c][0];
|
||||
|
||||
if (is_array($escapedValue)) {
|
||||
$escapedValue = '(' . implode(',', $escapedValue) . ')';
|
||||
}
|
||||
|
||||
$sql = substr_replace($sql, (string) $escapedValue, $matches[0][$c][1], $ml);
|
||||
} while ($c !== 0);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string to display in debug toolbar
|
||||
*/
|
||||
public function debugToolbarDisplay(): string
|
||||
{
|
||||
// Key words we want bolded
|
||||
static $highlight = [
|
||||
'AND',
|
||||
'AS',
|
||||
'ASC',
|
||||
'AVG',
|
||||
'BY',
|
||||
'COUNT',
|
||||
'DESC',
|
||||
'DISTINCT',
|
||||
'FROM',
|
||||
'GROUP',
|
||||
'HAVING',
|
||||
'IN',
|
||||
'INNER',
|
||||
'INSERT',
|
||||
'INTO',
|
||||
'IS',
|
||||
'JOIN',
|
||||
'LEFT',
|
||||
'LIKE',
|
||||
'LIMIT',
|
||||
'MAX',
|
||||
'MIN',
|
||||
'NOT',
|
||||
'NULL',
|
||||
'OFFSET',
|
||||
'ON',
|
||||
'OR',
|
||||
'ORDER',
|
||||
'RIGHT',
|
||||
'SELECT',
|
||||
'SUM',
|
||||
'UPDATE',
|
||||
'VALUES',
|
||||
'WHERE',
|
||||
];
|
||||
|
||||
$sql = esc($this->getQuery());
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/a/20767160
|
||||
* @see https://regex101.com/r/hUlrGN/4
|
||||
*/
|
||||
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/';
|
||||
|
||||
return preg_replace_callback($search, static fn ($matches): string => '<strong>' . str_replace(' ', ' ', $matches[0]) . '</strong>', $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return text representation of the query
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Interface QueryInterface
|
||||
*
|
||||
* Represents a single statement that can be executed against the database.
|
||||
* Statements are platform-specific and can handle binding of binds.
|
||||
*/
|
||||
interface QueryInterface
|
||||
{
|
||||
/**
|
||||
* Sets the raw query string to use for this statement.
|
||||
*
|
||||
* @param mixed $binds
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(string $sql, $binds = null, bool $setEscape = true);
|
||||
|
||||
/**
|
||||
* Returns the final, processed query string after binding, etal
|
||||
* has been performed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQuery();
|
||||
|
||||
/**
|
||||
* Records the execution time of the statement using microtime(true)
|
||||
* for it's start and end values. If no end value is present, will
|
||||
* use the current time to determine total duration.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDuration(float $start, ?float $end = null);
|
||||
|
||||
/**
|
||||
* Returns the duration of this query during execution, or null if
|
||||
* the query has not been executed yet.
|
||||
*
|
||||
* @param int $decimals The accuracy of the returned time.
|
||||
*/
|
||||
public function getDuration(int $decimals = 6): string;
|
||||
|
||||
/**
|
||||
* Stores the error description that happened for this query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setError(int $code, string $error);
|
||||
|
||||
/**
|
||||
* Reports whether this statement created an error not.
|
||||
*/
|
||||
public function hasError(): bool;
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int;
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string;
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*/
|
||||
public function isWriteType(): bool;
|
||||
|
||||
/**
|
||||
* Swaps out one table prefix for a new one.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function swapPrefix(string $orig, string $swap);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* @see \CodeIgniter\Database\RawSqlTest
|
||||
*/
|
||||
class RawSql implements Stringable
|
||||
{
|
||||
/**
|
||||
* @var string Raw SQL string
|
||||
*/
|
||||
private string $string;
|
||||
|
||||
public function __construct(string $sqlString)
|
||||
{
|
||||
$this->string = $sqlString;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance with new SQL string
|
||||
*/
|
||||
public function with(string $newSqlString): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->string = $newSqlString;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unique id for binding key
|
||||
*/
|
||||
public function getBindingKey(): string
|
||||
{
|
||||
return 'RawSql' . spl_object_id($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @template TConnection
|
||||
* @template TResult
|
||||
*/
|
||||
interface ResultInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve the results of the query. Typically an array of
|
||||
* individual data rows, which can be either an 'array', an
|
||||
* 'object', or a custom class name.
|
||||
*
|
||||
* @param string $type The row type. Either 'array', 'object', or a class name to use
|
||||
*/
|
||||
public function getResult(string $type = 'object'): array;
|
||||
|
||||
/**
|
||||
* Returns the results as an array of custom objects.
|
||||
*
|
||||
* @param string $className The name of the class to use.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomResultObject(string $className);
|
||||
|
||||
/**
|
||||
* Returns the results as an array of arrays.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultArray(): array;
|
||||
|
||||
/**
|
||||
* Returns the results as an array of objects.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultObject(): array;
|
||||
|
||||
/**
|
||||
* Wrapper object to return a row as either an array, an object, or
|
||||
* a custom class.
|
||||
*
|
||||
* If the row doesn't exist, returns null.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int|string $n The index of the results to return, or column name.
|
||||
* @param 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
|
||||
*/
|
||||
public function getRow($n = 0, string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns a row as a custom class instance.
|
||||
*
|
||||
* If the row doesn't exist, returns null.
|
||||
*
|
||||
* @template T of object
|
||||
*
|
||||
* @param int $n The index of the results to return.
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function getCustomRowObject(int $n, string $className);
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an array.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getRowArray(int $n = 0);
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an object.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return object|stdClass|null
|
||||
*/
|
||||
public function getRowObject(int $n = 0);
|
||||
|
||||
/**
|
||||
* Assigns an item into a particular column slot.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param array|object|stdClass|null $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRow($key, $value = null);
|
||||
|
||||
/**
|
||||
* Returns the "first" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getFirstRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "last" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getLastRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "next" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getNextRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "previous" row of the current results.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getPreviousRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns number of rows in the result set.
|
||||
*/
|
||||
public function getNumRows(): int;
|
||||
|
||||
/**
|
||||
* Returns an unbuffered row and move the pointer to the next row.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function getUnbufferedRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int;
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array;
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array;
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult();
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function dataSeek(int $n = 0);
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Builder for SQLite3
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* Default installs of SQLite typically do not
|
||||
* support limiting delete clauses.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitDeletes = false;
|
||||
|
||||
/**
|
||||
* Default installs of SQLite do no support
|
||||
* limiting update queries in combo with WHERE.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitWhereUpdates = false;
|
||||
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'RANDOM()',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'insert' => 'OR IGNORE',
|
||||
];
|
||||
|
||||
/**
|
||||
* Replace statement
|
||||
*
|
||||
* Generates a platform-specific replace string from the supplied data
|
||||
*/
|
||||
protected function _replace(string $table, array $keys, array $values): string
|
||||
{
|
||||
return 'INSERT OR ' . parent::_replace($table, $keys, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the TRUNCATE statement,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'DELETE FROM ' . $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
if (version_compare($this->db->getVersion(), '3.33.0') >= 0) {
|
||||
return parent::_updateBatch($table, $keys, $values);
|
||||
}
|
||||
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) {
|
||||
throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.');
|
||||
}
|
||||
|
||||
$index = current($constraints);
|
||||
|
||||
$ids = [];
|
||||
$final = [];
|
||||
|
||||
foreach ($values as $val) {
|
||||
$val = array_combine($keys, $val);
|
||||
|
||||
$ids[] = $val[$index];
|
||||
|
||||
foreach (array_keys($val) as $field) {
|
||||
if ($field !== $index) {
|
||||
$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cases = '';
|
||||
|
||||
foreach ($final as $k => $v) {
|
||||
$cases .= $k . " = CASE \n"
|
||||
. implode("\n", $v) . "\n"
|
||||
. 'ELSE ' . $k . ' END, ';
|
||||
}
|
||||
|
||||
$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
|
||||
|
||||
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific upsertBatch string from the supplied data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _upsertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if (empty($constraints)) {
|
||||
$fieldNames = array_map(static fn ($columnName): string => trim($columnName, '`'), $keys);
|
||||
|
||||
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
|
||||
});
|
||||
|
||||
foreach ($allIndexes as $index) {
|
||||
$constraints = $index->fields;
|
||||
break;
|
||||
}
|
||||
|
||||
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
|
||||
}
|
||||
|
||||
if (empty($constraints)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('No constraint found for upsert.');
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '`excluded`';
|
||||
|
||||
if (strtolower($alias) !== '`excluded`') {
|
||||
throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.');
|
||||
}
|
||||
|
||||
$updateFields = $this->QBOptions['updateFields'] ??
|
||||
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
|
||||
[];
|
||||
|
||||
$sql = 'INSERT INTO ' . $table . ' (';
|
||||
|
||||
$sql .= implode(', ', array_map(static fn ($columnName): string => $columnName, $keys));
|
||||
|
||||
$sql .= ")\n";
|
||||
|
||||
$sql .= '{:_table_:}';
|
||||
|
||||
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
|
||||
|
||||
$sql .= "DO UPDATE SET\n";
|
||||
|
||||
$sql .= implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($key, $value): string => $key . ($value instanceof RawSql ?
|
||||
" = {$value}" :
|
||||
" = {$alias}.{$value}"),
|
||||
array_keys($updateFields),
|
||||
$updateFields,
|
||||
),
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0;
|
||||
|
||||
$data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n");
|
||||
} else {
|
||||
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _deleteBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$sql = 'DELETE FROM ' . $table . "\n";
|
||||
|
||||
if (current($constraints) instanceof RawSql && $this->db->DBDebug) {
|
||||
throw new DatabaseException('You cannot use RawSql for constraint in SQLite.');
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (is_string(current(array_keys($constraints)))) {
|
||||
$concat1 = implode(' || ', array_keys($constraints));
|
||||
$concat2 = implode(' || ', array_values($constraints));
|
||||
} else {
|
||||
$concat1 = implode(' || ', $constraints);
|
||||
$concat2 = $concat1;
|
||||
}
|
||||
|
||||
$sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))";
|
||||
|
||||
// where is not supported
|
||||
if ($this->QBWhere !== [] && $this->db->DBDebug) {
|
||||
throw new DatabaseException('You cannot use WHERE with SQLite.');
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = implode(
|
||||
" UNION ALL\n",
|
||||
array_map(
|
||||
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for SQLite3
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr = 'DROP INDEX %s';
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array|bool
|
||||
*/
|
||||
protected $_unsigned = false;
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
|
||||
if (version_compare($this->db->getVersion(), '3.3', '<')) {
|
||||
$this->dropTableIfStr = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database
|
||||
*
|
||||
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
|
||||
*/
|
||||
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
|
||||
{
|
||||
// In SQLite, a database is created when you connect to the database.
|
||||
// We'll return TRUE so that an error isn't generated.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropDatabase(string $dbName): bool
|
||||
{
|
||||
// In SQLite, a database is dropped when we delete a file
|
||||
if (! is_file($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to close the pseudo-connection first
|
||||
$this->db->close();
|
||||
if (! @unlink($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! empty($this->db->dataCache['db_names'])) {
|
||||
$key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
|
||||
if ($key !== false) {
|
||||
unset($this->db->dataCache['db_names'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string>|string $columnNames
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropColumn(string $table, $columnNames): bool
|
||||
{
|
||||
//$columns = is_array($columnNames) ? $columnNames : array_map(trim(...), explode(',', $columnNames));
|
||||
$columns = is_array($columnNames) ? $columnNames : array_map('trim', explode(',', $columnNames));
|
||||
$result = (new Table($this->db, $this))
|
||||
->fromTable($this->db->DBPrefix . $table)
|
||||
->dropColumn($columns)
|
||||
->run();
|
||||
|
||||
if (! $result && $this->db->DBDebug) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Failed to drop column%s "%s" on "%s" table.',
|
||||
count($columns) > 1 ? 's' : '',
|
||||
implode('", "', $columns),
|
||||
$table,
|
||||
));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $processedFields Processed column definitions
|
||||
* or column names to DROP
|
||||
*
|
||||
* @return ($alterType is 'DROP' ? string : list<string>|null)
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $processedFields)
|
||||
{
|
||||
switch ($alterType) {
|
||||
case 'CHANGE':
|
||||
$fieldsToModify = [];
|
||||
|
||||
foreach ($processedFields as $processedField) {
|
||||
$name = $processedField['name'];
|
||||
$newName = $processedField['new_name'];
|
||||
|
||||
$field = $this->fields[$name];
|
||||
$field['name'] = $name;
|
||||
$field['new_name'] = $newName;
|
||||
|
||||
// Unlike when creating a table, if `null` is not specified,
|
||||
// the column will be `NULL`, not `NOT NULL`.
|
||||
if ($processedField['null'] === '') {
|
||||
$field['null'] = true;
|
||||
}
|
||||
|
||||
$fieldsToModify[] = $field;
|
||||
}
|
||||
|
||||
(new Table($this->db, $this))
|
||||
->fromTable($table)
|
||||
->modifyColumn($fieldsToModify)
|
||||
->run();
|
||||
|
||||
return null; // Why null?
|
||||
|
||||
default:
|
||||
return parent::_alterTable($alterType, $table, $processedFields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $processedField): string
|
||||
{
|
||||
if ($processedField['type'] === 'TEXT' && str_starts_with($processedField['length'], "('")) {
|
||||
$processedField['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($processedField['name'])
|
||||
. ' IN ' . $processedField['length'] . ')';
|
||||
}
|
||||
|
||||
return $this->db->escapeIdentifiers($processedField['name'])
|
||||
. ' ' . $processedField['type']
|
||||
. $processedField['auto_increment']
|
||||
. $processedField['null']
|
||||
. $processedField['unique']
|
||||
. $processedField['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute TYPE
|
||||
*
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'ENUM':
|
||||
case 'SET':
|
||||
$attributes['TYPE'] = 'TEXT';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'INT';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (
|
||||
! empty($attributes['AUTO_INCREMENT'])
|
||||
&& $attributes['AUTO_INCREMENT'] === true
|
||||
&& str_contains(strtolower($field['type']), 'int')
|
||||
) {
|
||||
$field['type'] = 'INTEGER PRIMARY KEY';
|
||||
$field['default'] = '';
|
||||
$field['null'] = '';
|
||||
$field['unique'] = '';
|
||||
$field['auto_increment'] = ' AUTOINCREMENT';
|
||||
|
||||
$this->primaryKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreign Key Drop
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropForeignKey(string $table, string $foreignName): bool
|
||||
{
|
||||
// If this version of SQLite doesn't support it, we're done here
|
||||
if ($this->db->supportsForeignKeys() !== true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we have to copy the table and recreate
|
||||
// without the foreign key being involved now
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
return $sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->dropForeignKey($foreignName)
|
||||
->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Primary Key
|
||||
*/
|
||||
public function dropPrimaryKey(string $table, string $keyName = ''): bool
|
||||
{
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
return $sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->dropPrimaryKey()
|
||||
->run();
|
||||
}
|
||||
|
||||
public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = ''): BaseForge
|
||||
{
|
||||
if ($fkName === '') {
|
||||
return parent::addForeignKey($fieldName, $tableName, $tableField, $onUpdate, $onDelete, $fkName);
|
||||
}
|
||||
|
||||
throw new DatabaseException('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL to add primary key
|
||||
*
|
||||
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
|
||||
*/
|
||||
protected function _processPrimaryKeys(string $table, bool $asQuery = false): string
|
||||
{
|
||||
if ($asQuery === false) {
|
||||
return parent::_processPrimaryKeys($table, $asQuery);
|
||||
}
|
||||
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->addPrimaryKey($this->primaryKeys)
|
||||
->run();
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL to add foreign keys
|
||||
*
|
||||
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
|
||||
*/
|
||||
protected function _processForeignKeys(string $table, bool $asQuery = false): array
|
||||
{
|
||||
if ($asQuery === false) {
|
||||
return parent::_processForeignKeys($table, $asQuery);
|
||||
}
|
||||
|
||||
$errorNames = [];
|
||||
|
||||
foreach ($this->foreignKeys as $name) {
|
||||
foreach ($name['field'] as $f) {
|
||||
if (! isset($this->fields[$f])) {
|
||||
$errorNames[] = $f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errorNames !== []) {
|
||||
$errorNames = [implode(', ', $errorNames)];
|
||||
|
||||
throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
|
||||
}
|
||||
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->addForeignKey($this->foreignKeys)
|
||||
->run();
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use Exception;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use SQLite3Stmt;
|
||||
|
||||
/**
|
||||
* Prepared query for SQLite3
|
||||
*
|
||||
* @extends BasePreparedQuery<SQLite3, SQLite3Stmt, SQLite3Result>
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* The SQLite3Result resource, or false.
|
||||
*
|
||||
* @var false|SQLite3Result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = []): PreparedQuery
|
||||
{
|
||||
if (! ($this->statement = $this->db->connID->prepare($sql))) {
|
||||
$this->errorCode = $this->db->connID->lastErrorCode();
|
||||
$this->errorString = $this->db->connID->lastErrorMsg();
|
||||
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
foreach ($data as $key => $item) {
|
||||
// Determine the type string
|
||||
if (is_int($item)) {
|
||||
$bindType = SQLITE3_INTEGER;
|
||||
} elseif (is_float($item)) {
|
||||
$bindType = SQLITE3_FLOAT;
|
||||
} elseif (is_string($item) && $this->isBinary($item)) {
|
||||
$bindType = SQLITE3_BLOB;
|
||||
} else {
|
||||
$bindType = SQLITE3_TEXT;
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bindValue($key + 1, $item, $bindType);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->result = $this->statement->execute();
|
||||
} catch (Exception $e) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query or false on failure.
|
||||
*
|
||||
* @return false|SQLite3Result
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate prepared statements.
|
||||
*/
|
||||
protected function _close(): bool
|
||||
{
|
||||
return $this->statement->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for SQLite3
|
||||
*
|
||||
* @extends BaseResult<SQLite3, SQLite3Result>
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return $this->resultID->numColumns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$fieldNames[] = $this->resultID->columnName($i);
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
SQLITE3_INTEGER => 'integer',
|
||||
SQLITE3_FLOAT => 'float',
|
||||
SQLITE3_TEXT => 'text',
|
||||
SQLITE3_BLOB => 'blob',
|
||||
SQLITE3_NULL => 'null',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
$this->resultID->fetchArray(SQLITE3_NUM);
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $this->resultID->columnName($i);
|
||||
$type = $this->resultID->columnType($i);
|
||||
$retVal[$i]->type = $type;
|
||||
$retVal[$i]->type_name = $dataTypes[$type] ?? null;
|
||||
$retVal[$i]->max_length = null;
|
||||
$retVal[$i]->length = null;
|
||||
}
|
||||
$this->resultID->reset();
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_object($this->resultID)) {
|
||||
$this->resultID->finalize();
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
if ($n !== 0) {
|
||||
throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.');
|
||||
}
|
||||
|
||||
return $this->resultID->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return $this->resultID->fetchArray(SQLITE3_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return Entity|false|object|stdClass
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
// No native support for fetching rows as objects
|
||||
if (($row = $this->fetchAssoc()) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($className === 'stdClass') {
|
||||
return (object) $row;
|
||||
}
|
||||
|
||||
$classObj = new $className();
|
||||
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return $classObj->injectRawData($row);
|
||||
}
|
||||
|
||||
$classSet = Closure::bind(function ($key, $value): void {
|
||||
$this->{$key} = $value;
|
||||
}, $classObj, $className);
|
||||
|
||||
foreach (array_keys($row) as $key) {
|
||||
$classSet($key, $row[$key]);
|
||||
}
|
||||
|
||||
return $classObj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Provides missing features for altering tables that are common
|
||||
* in other supported databases, but are missing from SQLite.
|
||||
* These are needed in order to support migrations during testing
|
||||
* when another database is used as the primary engine, but
|
||||
* SQLite in memory databases are used for faster test execution.
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
/**
|
||||
* All of the fields this table represents.
|
||||
*
|
||||
* @var array<string, array<string, bool|int|string|null>> [name => attributes]
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* All of the unique/primary keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $keys = [];
|
||||
|
||||
/**
|
||||
* All of the foreign keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $foreignKeys = [];
|
||||
|
||||
/**
|
||||
* The name of the table we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* The name of the table, with database prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefixedTableName;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Handle to our forge.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* Table constructor.
|
||||
*/
|
||||
public function __construct(Connection $db, Forge $forge)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->forge = $forge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an existing database table and
|
||||
* collects all of the information needed to
|
||||
* recreate this table.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function fromTable(string $table)
|
||||
{
|
||||
$this->prefixedTableName = $table;
|
||||
|
||||
$prefix = $this->db->DBPrefix;
|
||||
|
||||
if (! empty($prefix) && str_starts_with($table, $prefix)) {
|
||||
$table = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists($this->prefixedTableName)) {
|
||||
throw DataException::forTableNotFound($this->prefixedTableName);
|
||||
}
|
||||
|
||||
$this->tableName = $table;
|
||||
|
||||
$this->fields = $this->formatFields($this->db->getFieldData($table));
|
||||
|
||||
$indexData = $this->db->getIndexData($table);
|
||||
$this->keys = array_merge($this->keys, $this->formatKeys(is_array($indexData) ? $indexData : []));
|
||||
|
||||
// if primary key index exists twice then remove psuedo index name 'primary'.
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => $index['type'] === 'primary');
|
||||
|
||||
if ($primaryIndexes !== [] && count($primaryIndexes) > 1 && array_key_exists('primary', $this->keys)) {
|
||||
unset($this->keys['primary']);
|
||||
}
|
||||
|
||||
$fkData = $this->db->getForeignKeyData($table);
|
||||
$this->foreignKeys = is_array($fkData) ? $fkData : [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after `fromTable` and any actions, like `dropColumn`, etc,
|
||||
* to finalize the action. It creates a temp table, creates the new
|
||||
* table with modifications, and copies the data over to the new table.
|
||||
* Resets the connection dataCache to be sure changes are collected.
|
||||
*/
|
||||
public function run(): bool
|
||||
{
|
||||
// 重建表前确保等待锁,避免立刻 database is locked
|
||||
if (method_exists($this->db, 'connID') || isset($this->db->connID)) {
|
||||
$conn = $this->db->connID ?? null;
|
||||
if ($conn instanceof \SQLite3) {
|
||||
$conn->busyTimeout(10000);
|
||||
@$conn->exec('PRAGMA busy_timeout = 10000');
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
$this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
|
||||
|
||||
$this->forge->reset();
|
||||
|
||||
$this->createTable();
|
||||
|
||||
$this->copyData();
|
||||
|
||||
$this->forge->dropTable("temp_{$this->tableName}");
|
||||
|
||||
$success = $this->db->transComplete();
|
||||
} catch (\Throwable $e) {
|
||||
// 必须回滚,否则同请求后续操作会一直 database is locked
|
||||
$this->db->transRollback();
|
||||
// 尽量恢复外键后再抛出
|
||||
try {
|
||||
$this->db->query('PRAGMA foreign_keys = ON');
|
||||
} catch (\Throwable $ignore) {
|
||||
}
|
||||
$this->db->resetDataCache();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->db->query('PRAGMA foreign_keys = ON');
|
||||
|
||||
$this->db->resetDataCache();
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops columns from the table.
|
||||
*
|
||||
* @param list<string>|string $columns Column names to drop.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropColumn($columns)
|
||||
{
|
||||
if (is_string($columns)) {
|
||||
$columns = explode(',', $columns);
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$column = trim($column);
|
||||
if (isset($this->fields[$column])) {
|
||||
unset($this->fields[$column]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a field, including changing data type, renaming, etc.
|
||||
*
|
||||
* @param list<array<string, bool|int|string|null>> $fieldsToModify
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function modifyColumn(array $fieldsToModify)
|
||||
{
|
||||
foreach ($fieldsToModify as $field) {
|
||||
$oldName = $field['name'];
|
||||
unset($field['name']);
|
||||
|
||||
$this->fields[$oldName] = $field;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the primary key
|
||||
*/
|
||||
public function dropPrimaryKey(): Table
|
||||
{
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
|
||||
|
||||
foreach (array_keys($primaryIndexes) as $key) {
|
||||
unset($this->keys[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a foreign key from this table so that
|
||||
* it won't be recreated in the future.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropForeignKey(string $foreignName)
|
||||
{
|
||||
if (empty($this->foreignKeys)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (isset($this->foreignKeys[$foreignName])) {
|
||||
unset($this->foreignKeys[$foreignName]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds primary key
|
||||
*/
|
||||
public function addPrimaryKey(array $fields): Table
|
||||
{
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
|
||||
|
||||
// if primary key already exists we can't add another one
|
||||
if ($primaryIndexes !== []) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// add array to keys of fields
|
||||
$pk = [
|
||||
'fields' => $fields['fields'],
|
||||
'type' => 'primary',
|
||||
];
|
||||
|
||||
$this->keys['primary'] = $pk;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a foreign key
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addForeignKey(array $foreignKeys)
|
||||
{
|
||||
$fk = [];
|
||||
|
||||
// convert to object
|
||||
foreach ($foreignKeys as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->column_name = $row['field'];
|
||||
$obj->foreign_table_name = $row['referenceTable'];
|
||||
$obj->foreign_column_name = $row['referenceField'];
|
||||
$obj->on_delete = $row['onDelete'];
|
||||
$obj->on_update = $row['onUpdate'];
|
||||
|
||||
$fk[] = $obj;
|
||||
}
|
||||
|
||||
$this->foreignKeys = array_merge($this->foreignKeys, $fk);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the new table based on our current fields.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function createTable()
|
||||
{
|
||||
$this->dropIndexes();
|
||||
$this->db->resetDataCache();
|
||||
|
||||
// Handle any modified columns.
|
||||
$fields = [];
|
||||
|
||||
foreach ($this->fields as $name => $field) {
|
||||
if (isset($field['new_name'])) {
|
||||
$fields[$field['new_name']] = $field;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[$name] = $field;
|
||||
}
|
||||
|
||||
$this->forge->addField($fields);
|
||||
|
||||
$fieldNames = array_keys($fields);
|
||||
|
||||
$this->keys = array_filter(
|
||||
$this->keys,
|
||||
static fn ($index): bool => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields']),
|
||||
);
|
||||
|
||||
// Unique/Index keys
|
||||
if (is_array($this->keys)) {
|
||||
foreach ($this->keys as $keyName => $key) {
|
||||
switch ($key['type']) {
|
||||
case 'primary':
|
||||
$this->forge->addPrimaryKey($key['fields']);
|
||||
break;
|
||||
|
||||
case 'unique':
|
||||
$this->forge->addUniqueKey($key['fields'], $keyName);
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
$this->forge->addKey($key['fields'], false, false, $keyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->foreignKeys as $foreignKey) {
|
||||
$this->forge->addForeignKey(
|
||||
$foreignKey->column_name,
|
||||
trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
|
||||
$foreignKey->foreign_column_name,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->forge->createTable($this->tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data from our old table to the new one,
|
||||
* taking care map data correctly based on any columns
|
||||
* that have been renamed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function copyData()
|
||||
{
|
||||
$exFields = [];
|
||||
$newFields = [];
|
||||
|
||||
foreach ($this->fields as $name => $details) {
|
||||
$newFields[] = $details['new_name'] ?? $name;
|
||||
$exFields[] = $name;
|
||||
}
|
||||
|
||||
$exFields = implode(
|
||||
', ',
|
||||
array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields),
|
||||
);
|
||||
$newFields = implode(
|
||||
', ',
|
||||
array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields),
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
"INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fields retrieved from the database to
|
||||
* the format needed for creating fields with Forge.
|
||||
*
|
||||
* @param array|bool $fields
|
||||
*
|
||||
* @return ($fields is array ? array : mixed)
|
||||
*/
|
||||
protected function formatFields($fields)
|
||||
{
|
||||
if (! is_array($fields)) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$return[$field->name] = [
|
||||
'type' => $field->type,
|
||||
'default' => $field->default,
|
||||
'null' => $field->nullable,
|
||||
];
|
||||
|
||||
if ($field->default === null) {
|
||||
// `null` means that the default value is not defined.
|
||||
unset($return[$field->name]['default']);
|
||||
} elseif ($field->default === 'NULL') {
|
||||
// 'NULL' means that the default value is NULL.
|
||||
$return[$field->name]['default'] = null;
|
||||
} else {
|
||||
$default = trim($field->default, "'");
|
||||
|
||||
if ($this->isIntegerType($field->type)) {
|
||||
$default = (int) $default;
|
||||
} elseif ($this->isNumericType($field->type)) {
|
||||
$default = (float) $default;
|
||||
}
|
||||
|
||||
$return[$field->name]['default'] = $default;
|
||||
}
|
||||
|
||||
if ($field->primary_key) {
|
||||
$this->keys['primary'] = [
|
||||
'fields' => [$field->name],
|
||||
'type' => 'primary',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is INTEGER type?
|
||||
*
|
||||
* @param string $type SQLite data type (case-insensitive)
|
||||
*
|
||||
* @see https://www.sqlite.org/datatype3.html
|
||||
*/
|
||||
private function isIntegerType(string $type): bool
|
||||
{
|
||||
return str_contains(strtoupper($type), 'INT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is NUMERIC type?
|
||||
*
|
||||
* @param string $type SQLite data type (case-insensitive)
|
||||
*
|
||||
* @see https://www.sqlite.org/datatype3.html
|
||||
*/
|
||||
private function isNumericType(string $type): bool
|
||||
{
|
||||
return in_array(strtoupper($type), ['NUMERIC', 'DECIMAL'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts keys retrieved from the database to
|
||||
* the format needed to create later.
|
||||
*
|
||||
* @param array<string, stdClass> $keys
|
||||
*
|
||||
* @return array<string, array{fields: string, type: string}>
|
||||
*/
|
||||
protected function formatKeys($keys)
|
||||
{
|
||||
$return = [];
|
||||
|
||||
foreach ($keys as $name => $key) {
|
||||
$return[strtolower($name)] = [
|
||||
'fields' => $key->fields,
|
||||
'type' => strtolower($key->type),
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to drop all indexes and constraints
|
||||
* from the database for this table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function dropIndexes()
|
||||
{
|
||||
if (! is_array($this->keys) || $this->keys === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($this->keys) as $name) {
|
||||
if ($name === 'primary') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for SQLite3
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'REINDEX %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use Config\Database;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
|
||||
/**
|
||||
* Class Seeder
|
||||
*/
|
||||
class Seeder
|
||||
{
|
||||
/**
|
||||
* The name of the database group to use.
|
||||
*
|
||||
* @var non-empty-string
|
||||
*/
|
||||
protected $DBGroup;
|
||||
|
||||
/**
|
||||
* Where we can find the Seed files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $seedPath;
|
||||
|
||||
/**
|
||||
* An instance of the main Database configuration
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Database Connection instance
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Database Forge instance.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* If true, will not display CLI messages.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* Faker Generator instance.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
private static ?Generator $faker = null;
|
||||
|
||||
/**
|
||||
* Seeder constructor.
|
||||
*/
|
||||
public function __construct(Database $config, ?BaseConnection $db = null)
|
||||
{
|
||||
$this->seedPath = $config->filesPath ?? APPPATH . 'Database/';
|
||||
|
||||
if ($this->seedPath === '') {
|
||||
throw new InvalidArgumentException('Invalid filesPath set in the Config\Database.');
|
||||
}
|
||||
|
||||
$this->seedPath = rtrim($this->seedPath, '\\/') . '/Seeds/';
|
||||
|
||||
if (! is_dir($this->seedPath)) {
|
||||
throw new InvalidArgumentException('Unable to locate the seeds directory. Please check Config\Database::filesPath');
|
||||
}
|
||||
|
||||
$this->config = &$config;
|
||||
|
||||
$db ??= Database::connect($this->DBGroup);
|
||||
|
||||
$this->db = $db;
|
||||
$this->forge = Database::forge($this->DBGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Faker Generator instance.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public static function faker(): ?Generator
|
||||
{
|
||||
if (! self::$faker instanceof Generator && class_exists(Factory::class)) {
|
||||
self::$faker = Factory::create();
|
||||
}
|
||||
|
||||
return self::$faker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified seeder and runs it.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function call(string $class)
|
||||
{
|
||||
$class = trim($class);
|
||||
|
||||
if ($class === '') {
|
||||
throw new InvalidArgumentException('No seeder was specified.');
|
||||
}
|
||||
|
||||
if (! str_contains($class, '\\')) {
|
||||
$path = $this->seedPath . str_replace('.php', '', $class) . '.php';
|
||||
|
||||
if (! is_file($path)) {
|
||||
throw new InvalidArgumentException('The specified seeder is not a valid file: ' . $path);
|
||||
}
|
||||
|
||||
// Assume the class has the correct namespace
|
||||
// @codeCoverageIgnoreStart
|
||||
$class = APP_NAMESPACE . '\Database\Seeds\\' . $class;
|
||||
|
||||
if (! class_exists($class, false)) {
|
||||
require_once $path;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/** @var Seeder $seeder */
|
||||
$seeder = new $class($this->config);
|
||||
$seeder->setSilent($this->silent)->run();
|
||||
|
||||
unset($seeder);
|
||||
|
||||
if (is_cli() && ! $this->silent) {
|
||||
CLI::write("Seeded: {$class}", 'green');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location of the directory that seed files can be located in.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPath(string $path)
|
||||
{
|
||||
$this->seedPath = rtrim($path, '\\/') . '/';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the silent treatment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSilent(bool $silent)
|
||||
{
|
||||
$this->silent = $silent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the database seeds. This is where the magic happens.
|
||||
*
|
||||
* Child classes must implement this method and take care
|
||||
* of inserting their data here.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Represents a table name in SQL.
|
||||
*
|
||||
* @interal
|
||||
*
|
||||
* @see \CodeIgniter\Database\TableNameTest
|
||||
*/
|
||||
class TableName
|
||||
{
|
||||
/**
|
||||
* @param string $actualTable Actual table name
|
||||
* @param string $logicalTable Logical table name (w/o DB prefix)
|
||||
* @param string $schema Schema name
|
||||
* @param string $database Database name
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
protected function __construct(
|
||||
private readonly string $actualTable,
|
||||
private readonly string $logicalTable = '',
|
||||
private readonly string $schema = '',
|
||||
private readonly string $database = '',
|
||||
private readonly string $alias = '',
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param string $table Table name (w/o DB prefix)
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function create(string $dbPrefix, string $table, string $alias = ''): self
|
||||
{
|
||||
return new self(
|
||||
$dbPrefix . $table,
|
||||
$table,
|
||||
'',
|
||||
'',
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from an actual table name.
|
||||
*
|
||||
* @param string $actualTable Actual table name with DB prefix
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function fromActualName(string $dbPrefix, string $actualTable, string $alias = ''): self
|
||||
{
|
||||
$prefix = $dbPrefix;
|
||||
$logicalTable = '';
|
||||
|
||||
if (str_starts_with($actualTable, $prefix)) {
|
||||
$logicalTable = substr($actualTable, strlen($prefix));
|
||||
}
|
||||
|
||||
return new self(
|
||||
$actualTable,
|
||||
$logicalTable,
|
||||
'',
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from full name.
|
||||
*
|
||||
* @param string $table Table name (w/o DB prefix)
|
||||
* @param string $schema Schema name
|
||||
* @param string $database Database name
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function fromFullName(
|
||||
string $dbPrefix,
|
||||
string $table,
|
||||
string $schema = '',
|
||||
string $database = '',
|
||||
string $alias = '',
|
||||
): self {
|
||||
return new self(
|
||||
$dbPrefix . $table,
|
||||
$table,
|
||||
$schema,
|
||||
$database,
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single segment table name w/o DB prefix.
|
||||
*/
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->logicalTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual single segment table name w/z DB prefix.
|
||||
*/
|
||||
public function getActualTableName(): string
|
||||
{
|
||||
return $this->actualTable;
|
||||
}
|
||||
|
||||
public function getAlias(): string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function getSchema(): string
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
public function getDatabase(): string
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Exceptions as ExceptionsConfig;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Provides common functions for exception handlers,
|
||||
* especially around displaying the output.
|
||||
*/
|
||||
abstract class BaseExceptionHandler
|
||||
{
|
||||
/**
|
||||
* Config for debug exceptions.
|
||||
*/
|
||||
protected ExceptionsConfig $config;
|
||||
|
||||
/**
|
||||
* Nesting level of the output buffering mechanism
|
||||
*/
|
||||
protected int $obLevel;
|
||||
|
||||
/**
|
||||
* The path to the directory containing the
|
||||
* cli and html error view directories.
|
||||
*/
|
||||
protected ?string $viewPath = null;
|
||||
|
||||
public function __construct(ExceptionsConfig $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
$this->obLevel = ob_get_level();
|
||||
|
||||
if ($this->viewPath === null) {
|
||||
$this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point into the handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function handle(
|
||||
Throwable $exception,
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
int $statusCode,
|
||||
int $exitCode,
|
||||
);
|
||||
|
||||
/**
|
||||
* Gathers the variables that will be made available to the view.
|
||||
*/
|
||||
protected function collectVars(Throwable $exception, int $statusCode): array
|
||||
{
|
||||
// Get the first exception.
|
||||
$firstException = $exception;
|
||||
|
||||
while ($prevException = $firstException->getPrevious()) {
|
||||
$firstException = $prevException;
|
||||
}
|
||||
|
||||
$trace = $firstException->getTrace();
|
||||
|
||||
if ($this->config->sensitiveDataInTrace !== []) {
|
||||
$trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $exception::class,
|
||||
'type' => $exception::class,
|
||||
'code' => $statusCode,
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $trace,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask sensitive data in the trace.
|
||||
*/
|
||||
protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array
|
||||
{
|
||||
foreach ($trace as $i => $line) {
|
||||
$trace[$i]['args'] = $this->maskData($line['args'], $keysToMask);
|
||||
}
|
||||
|
||||
return $trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|object $args
|
||||
*
|
||||
* @return array|object
|
||||
*/
|
||||
private function maskData($args, array $keysToMask, string $path = '')
|
||||
{
|
||||
foreach ($keysToMask as $keyToMask) {
|
||||
$explode = explode('/', $keyToMask);
|
||||
$index = end($explode);
|
||||
|
||||
if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
|
||||
if (is_array($args) && array_key_exists($index, $args)) {
|
||||
$args[$index] = '******************';
|
||||
} elseif (
|
||||
is_object($args) && property_exists($args, $index)
|
||||
&& isset($args->{$index}) && is_scalar($args->{$index})
|
||||
) {
|
||||
$args->{$index} = '******************';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($args)) {
|
||||
foreach ($args as $pathKey => $subarray) {
|
||||
$args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
|
||||
}
|
||||
} elseif (is_object($args)) {
|
||||
foreach ($args as $pathKey => $subarray) {
|
||||
$args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes memory usage in real-world units. Intended for use
|
||||
* with memory_get_usage, etc.
|
||||
*
|
||||
* @used-by app/Views/errors/html/error_exception.php
|
||||
*/
|
||||
protected static function describeMemory(int $bytes): string
|
||||
{
|
||||
helper('number');
|
||||
|
||||
return number_to_size($bytes, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a syntax-highlighted version of a PHP file.
|
||||
*
|
||||
* @used-by app/Views/errors/html/error_exception.php
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
protected static function highlightFile(string $file, int $lineNumber, int $lines = 15)
|
||||
{
|
||||
if ($file === '' || ! is_readable($file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set our highlight colors:
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('highlight.comment', '#767a7e; font-style: italic');
|
||||
ini_set('highlight.default', '#c7c7c7');
|
||||
ini_set('highlight.html', '#06B');
|
||||
ini_set('highlight.keyword', '#f1ce61;');
|
||||
ini_set('highlight.string', '#869d6a');
|
||||
}
|
||||
|
||||
try {
|
||||
$source = file_get_contents($file);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$source = str_replace(["\r\n", "\r"], "\n", $source);
|
||||
$source = explode("\n", highlight_string($source, true));
|
||||
|
||||
if (PHP_VERSION_ID < 80300) {
|
||||
$source = str_replace('<br />', "\n", $source[1]);
|
||||
$source = explode("\n", str_replace("\r\n", "\n", $source));
|
||||
} else {
|
||||
// We have to remove these tags since we're preparing the result
|
||||
// ourselves and these tags are added manually at the end.
|
||||
$source = str_replace(['<pre><code>', '</code></pre>'], '', $source);
|
||||
}
|
||||
|
||||
// Get just the part to show
|
||||
$start = max($lineNumber - (int) round($lines / 2), 0);
|
||||
|
||||
// Get just the lines we need to display, while keeping line numbers...
|
||||
$source = array_splice($source, $start, $lines, true);
|
||||
|
||||
// Used to format the line number in the source
|
||||
$format = '% ' . strlen((string) ($start + $lines)) . 'd';
|
||||
|
||||
$out = '';
|
||||
// Because the highlighting may have an uneven number
|
||||
// of open and close span tags on one line, we need
|
||||
// to ensure we can close them all to get the lines
|
||||
// showing correctly.
|
||||
$spans = 0;
|
||||
|
||||
foreach ($source as $n => $row) {
|
||||
$spans += substr_count($row, '<span') - substr_count($row, '</span');
|
||||
$row = str_replace(["\r", "\n"], ['', ''], $row);
|
||||
|
||||
if (($n + $start + 1) === $lineNumber) {
|
||||
preg_match_all('#<[^>]+>#', $row, $tags);
|
||||
|
||||
$out .= sprintf(
|
||||
"<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
|
||||
$n + $start + 1,
|
||||
strip_tags($row),
|
||||
implode('', $tags[0]),
|
||||
);
|
||||
} else {
|
||||
$out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
|
||||
// We're closing only one span tag we added manually line before,
|
||||
// so we have to increment $spans count to close this tag later.
|
||||
$spans++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($spans > 0) {
|
||||
$out .= str_repeat('</span>', $spans);
|
||||
}
|
||||
|
||||
return '<pre><code>' . $out . '</code></pre>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an exception and status code will display the error to the client.
|
||||
*
|
||||
* @param string|null $viewFile
|
||||
*/
|
||||
protected function render(Throwable $exception, int $statusCode, $viewFile = null): void
|
||||
{
|
||||
if ($viewFile === null) {
|
||||
echo 'The error view file was not specified. Cannot display error view.';
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (! is_file($viewFile)) {
|
||||
echo 'The error view file "' . $viewFile . '" was not found. Cannot display error view.';
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo (function () use ($exception, $statusCode, $viewFile): string {
|
||||
$vars = $this->collectVars($exception, $statusCode);
|
||||
extract($vars, EXTR_SKIP);
|
||||
|
||||
// CLI error views output to STDERR/STDOUT, so ob_start() does not work.
|
||||
ob_clean(); //屏蔽之前输出
|
||||
|
||||
ob_start();
|
||||
include $viewFile;
|
||||
|
||||
return ob_get_clean();
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @see \CodeIgniter\Debug\ExceptionHandlerTest
|
||||
*/
|
||||
final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* ResponseTrait needs this.
|
||||
*/
|
||||
private $request = null;
|
||||
|
||||
/**
|
||||
* ResponseTrait needs this.
|
||||
*/
|
||||
private $response = null;
|
||||
|
||||
/**
|
||||
* Determines the correct way to display the error.
|
||||
*/
|
||||
public function handle(
|
||||
$exception,
|
||||
$request,
|
||||
$response,
|
||||
int $statusCode,
|
||||
int $exitCode,
|
||||
): void {
|
||||
// ResponseTrait needs these properties.
|
||||
|
||||
// Determine possible directories of error views
|
||||
$addPath = (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
|
||||
$path = $this->viewPath . $addPath;
|
||||
// Determine the views
|
||||
$view = $this->determineView($exception, $path, $statusCode);
|
||||
|
||||
// Check if the view exists
|
||||
$viewFile = null;
|
||||
if (is_file($path . $view)) {
|
||||
$viewFile = $path . $view;
|
||||
}
|
||||
|
||||
// Displays the HTML or CLI error code.
|
||||
$this->render($exception, $statusCode, $viewFile);
|
||||
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the view to display based on the exception thrown, HTTP status
|
||||
* code, whether an HTTP or CLI request, etc.
|
||||
*
|
||||
* @return string The filename of the view file to use
|
||||
*/
|
||||
protected function determineView(
|
||||
Throwable $exception,
|
||||
string $templatePath,
|
||||
int $statusCode = 500,
|
||||
): string {
|
||||
// Production environments should have a custom exception file.
|
||||
$view = 'production.php';
|
||||
|
||||
if ($this->isDisplayErrorsEnabled()) {
|
||||
// If display_errors is enabled, shows the error details.
|
||||
$view = 'error_exception.php';
|
||||
}
|
||||
|
||||
// 404 Errors
|
||||
if ($exception instanceof PageNotFoundException) {
|
||||
return 'error_404.php';
|
||||
}
|
||||
|
||||
$templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Allow for custom views based upon the status code
|
||||
if (is_file($templatePath . 'error_' . $statusCode . '.php')) {
|
||||
return 'error_' . $statusCode . '.php';
|
||||
}
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
private function isDisplayErrorsEnabled(): bool
|
||||
{
|
||||
return in_array(
|
||||
strtolower(ini_get('display_errors')),
|
||||
['1', 'true', 'on', 'yes'],
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Determines the correct way to display the error.
|
||||
*/
|
||||
public function handle(
|
||||
Throwable $exception,
|
||||
$request,
|
||||
$response,
|
||||
int $statusCode,
|
||||
int $exitCode,
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\Exceptions\HasExitCodeInterface;
|
||||
use CodeIgniter\Exceptions\HTTPExceptionInterface;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use Config\Exceptions as ExceptionsConfig;
|
||||
use ErrorException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exceptions manager
|
||||
*
|
||||
* @see \CodeIgniter\Debug\ExceptionsTest
|
||||
*/
|
||||
class Exceptions
|
||||
{
|
||||
|
||||
/**
|
||||
* Nesting level of the output buffering mechanism
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
public $ob_level;
|
||||
|
||||
/**
|
||||
* The path to the directory containing the
|
||||
* cli and html error view directories.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
protected $viewPath;
|
||||
|
||||
/**
|
||||
* Config for debug exceptions.
|
||||
*
|
||||
* @var ExceptionsConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The request.
|
||||
*
|
||||
* @var RequestInterface|null
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The outgoing response.
|
||||
*
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
private ?Throwable $exceptionCaughtByExceptionHandler = null;
|
||||
|
||||
public function __construct(ExceptionsConfig $config)
|
||||
{
|
||||
// For backward compatibility
|
||||
$this->ob_level = ob_get_level();
|
||||
$this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
|
||||
|
||||
$this->config = $config;
|
||||
|
||||
// workaround for upgraded users
|
||||
// This causes "Deprecated: Creation of dynamic property" in PHP 8.2.
|
||||
// @TODO remove this after dropping PHP 8.1 support.
|
||||
if (! isset($this->config->sensitiveDataInTrace)) {
|
||||
$this->config->sensitiveDataInTrace = [];
|
||||
}
|
||||
if (! isset($this->config->logDeprecations, $this->config->deprecationLogLevel)) {
|
||||
$this->config->logDeprecations = false;
|
||||
$this->config->deprecationLogLevel = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for registering the error, exception and shutdown
|
||||
* handling of our application.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
set_exception_handler([$this, 'exceptionHandler']);
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
//set_exception_handler($this->exceptionHandler(...));
|
||||
//set_error_handler($this->errorHandler(...));
|
||||
register_shutdown_function([$this, 'shutdownHandler']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文翻译输出的错误信息
|
||||
*/
|
||||
private function _cn_msg($message) {
|
||||
|
||||
if (!$message) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
if (strpos($message, 'Unable to connect to the database') !== false) {
|
||||
$message.= '<br>无法连接到数据库,检查数据库是否启动或者数据库配置文件不对,config/database.php';
|
||||
} elseif (strpos($message, 'Unclosed \'{\'') !== false) {
|
||||
$message.= '<br>循环体或者if语句,缺少结束语句,{ }没有成对出现';
|
||||
} elseif (strpos($message, 'Cannot access offset of type string on string') !== false) {
|
||||
$message.= '<br>此变量是字符串,不能使用数组的方式调用他,检查下代码语法';
|
||||
} elseif (strpos($message, 'Call to undefined function') !== false) {
|
||||
$message.= '<br>'.str_replace('Call to undefined function', '函数没有定义', $message);
|
||||
} elseif (strpos($message, 'open_basedir restriction in effect') !== false) {
|
||||
$message.= '<br>目录被限制读取,需要设置.users.ini文件中的目录白名单';
|
||||
} elseif (strpos($message, 'Undefined constant') !== false) {
|
||||
$message.= '<br>'.str_replace('Undefined constant', '变量或者常量没有定义', $message);
|
||||
} elseif (preg_match("/Table '(.+)' doesn't exist/", $message, $mt)) {
|
||||
$message.= '<br>数据库表'.$mt[1].'不存在,表丢失或者表没有创建成功';
|
||||
} elseif (preg_match("/Unknown column '(.+)' in 'field list'/", $message, $mt)) {
|
||||
$message.= '<br>表中没有字段'.$mt[1].',字段没有被创建';
|
||||
} elseif (preg_match("/Access level to (.+) must be protected \(as in class (.+)\) or weaker/U", $message, $mt)) {
|
||||
$message.= '<br>'.$mt[1].'在类'.$mt[2].'中已经被定义过更高级别的权限,请删除本文件的定义代码';
|
||||
} elseif (preg_match("/Creation of dynamic property (.+) is deprecated/", $message, $mt)) {
|
||||
$message.= '<br>动态属性被废除'.$mt[1].',请预先定义';
|
||||
} elseif (preg_match("/Failed opening required '(.+)'/", $message, $mt)) {
|
||||
$message.= '<br>文件'.$mt[1].'不存在,文件丢失或者文件没有创建成功';
|
||||
} elseif (preg_match("/syntax error, unexpected token (.+)/", $message, $mt)) {
|
||||
$message.= '<br>PHP语法错误 或者 模板标签语法错误,检查上下行代码是否写对';
|
||||
} elseif (preg_match("/Cannot declare class (.+), because the name is already in use/", $message, $mt)) {
|
||||
$message.= '<br>类名'.$mt[1].'重复,全文搜索下哪个地方被重复命名了';
|
||||
} elseif (preg_match("/Controller method is not found: (.+)/", $message, $mt)) {
|
||||
$message.= '<br>检查此文件中是否有'.$mt[1].'方法名:'.$this->_get_file();
|
||||
} elseif (preg_match("/Controller or its method is not found:(.+)/", $message, $mt)) {
|
||||
$message.= '<br>检查此文件是否存在:'.$this->_get_file().',检查地址是否正确,注意控制器文件首字母要大写';
|
||||
} elseif (preg_match("/count\(\): Argument #1 \((.+)\) must be of type Countable\|array/", $message, $mt)) {
|
||||
$message.= '<br>需要将count函数改为dr_count';
|
||||
} elseif (IS_XRDEV) {
|
||||
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 替换模板文件显示完整路径
|
||||
*/
|
||||
private function _rp_file($file) {
|
||||
|
||||
if (strpos((string)$file, '.cache.php') !== false && strpos((string)$file, '_DS_') !== false) {
|
||||
$file = str_replace([WRITEPATH.'template/', '_DS_', '.cache.php'], ['', '/', ''], $file);
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制器地址
|
||||
*/
|
||||
private function _get_file() {
|
||||
$file = APPPATH;
|
||||
if ($file == FRAMEPATH) {
|
||||
$file = CMSPATH.'Control';
|
||||
} else {
|
||||
$file.= 'Controllers';
|
||||
}
|
||||
|
||||
if (IS_ADMIN) {
|
||||
$file.= '/Admin';
|
||||
} elseif (IS_MEMBER) {
|
||||
$file.= '/Member';
|
||||
} elseif (IS_API) {
|
||||
$file.= '/Api';
|
||||
}
|
||||
|
||||
return $file.'/'.ucfirst(\Phpcmf\Service::L('Router')->class).'.php';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Catches any uncaught errors and exceptions, including most Fatal errors
|
||||
* (Yay PHP7!). Will log the error, display it if display_errors is on,
|
||||
* and fire an event that allows custom actions to be taken at this point.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function exceptionHandler(Throwable $exception)
|
||||
{
|
||||
|
||||
$message = $this->_cn_msg($exception->getMessage());
|
||||
// ajax 返回
|
||||
if (IS_AJAX || IS_API) {
|
||||
// 调试模式不屏蔽敏感信息
|
||||
$file = $exception->getFile();
|
||||
if (strpos($file, WRITEPATH.'template') !== false) {
|
||||
$file = $this->_rp_file($file);
|
||||
$message = '模板标签写法错误:'.$message;
|
||||
$arr = \Phpcmf\Service::V()->get_view_files();
|
||||
if ($arr) {
|
||||
$one = current($arr);
|
||||
$message.= '('.CI_DEBUG ? $one['path'] : basename($one['path']).')';
|
||||
}
|
||||
}
|
||||
if (CI_DEBUG) {
|
||||
$message.= '<br>错误文件:'.$file.'('.$exception->getLine().')';
|
||||
$message.= '<br>访问地址:'.\Phpcmf\Service::V()->now_php_url();
|
||||
$trace = $exception->getTrace();
|
||||
if ($trace) {
|
||||
foreach ($trace as $t) {
|
||||
if ($t['file'] && strpos((string)$t['file'], FRAMEPATH) === false) {
|
||||
$message.= '<br>'.$t['function'].':'.$t['file'].'('.$t['line'].')';
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$message = str_replace([FCPATH, WEBPATH], ['/', '/'], $message);
|
||||
}
|
||||
dr_exit_msg(0, $message);
|
||||
}
|
||||
|
||||
|
||||
$this->exceptionCaughtByExceptionHandler = null;
|
||||
|
||||
$this->exceptionCaughtByExceptionHandler = $exception;
|
||||
|
||||
[$statusCode, $exitCode] = $this->determineCodes($exception);
|
||||
|
||||
if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) {
|
||||
$uri = '';
|
||||
$routeInfo = '';
|
||||
|
||||
log_message('critical', $exception::class . ": {message}\n{routeInfo}\nin {exFile} on line {exLine}.\n{trace}", [
|
||||
'message' => $exception->getMessage(),
|
||||
'routeInfo' => $routeInfo,
|
||||
'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file
|
||||
'exLine' => $exception->getLine(), // {line} refers to THIS line
|
||||
'trace' => self::renderBacktrace($exception->getTrace()),
|
||||
]);
|
||||
|
||||
// Get the first exception.
|
||||
$last = $exception;
|
||||
|
||||
while ($prevException = $last->getPrevious()) {
|
||||
$last = $prevException;
|
||||
|
||||
log_message('critical', '[Caused by] ' . $prevException::class . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [
|
||||
'message' => $prevException->getMessage(),
|
||||
'exFile' => clean_path($prevException->getFile()), // {file} refers to THIS file
|
||||
'exLine' => $prevException->getLine(), // {line} refers to THIS line
|
||||
'trace' => self::renderBacktrace($prevException->getTrace()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (method_exists($this->config, 'handler')) {
|
||||
// Use new ExceptionHandler
|
||||
$handler = $this->config->handler($statusCode, $exception);
|
||||
$handler->handle(
|
||||
$exception,
|
||||
null,
|
||||
null,
|
||||
$statusCode,
|
||||
$exitCode,
|
||||
);
|
||||
|
||||
return;
|
||||
} else {
|
||||
exit($exception->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback to be registered to `set_error_handler()`.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws ErrorException
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null)
|
||||
{
|
||||
if ($this->isDeprecationError($severity)) {
|
||||
if ($this->isSessionSidDeprecationError($message, $file, $line)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isImplicitNullableDeprecationError($message, $file, $line)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) {
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
}
|
||||
|
||||
return $this->handleDeprecationError($message, $file, $line);
|
||||
}
|
||||
|
||||
if ((error_reporting() & $severity) !== 0) {
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
}
|
||||
|
||||
return false; // return false to propagate the error to PHP standard error handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles session.sid_length and session.sid_bits_per_character deprecations
|
||||
* in PHP 8.4.
|
||||
*/
|
||||
private function isSessionSidDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
|
||||
{
|
||||
if (
|
||||
PHP_VERSION_ID >= 80400
|
||||
&& str_contains($message, 'session.sid_')
|
||||
) {
|
||||
log_message(
|
||||
'wranting',
|
||||
'[DEPRECATED] {message} in {errFile} on line {errLine}.',
|
||||
[
|
||||
'message' => $message,
|
||||
'errFile' => clean_path($file ?? ''),
|
||||
'errLine' => $line ?? 0,
|
||||
],
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround to implicit nullable deprecation errors in PHP 8.4.
|
||||
*
|
||||
* "Implicitly marking parameter $xxx as nullable is deprecated,
|
||||
* the explicit nullable type must be used instead"
|
||||
*
|
||||
* @TODO remove this before v4.6.0 release
|
||||
*/
|
||||
private function isImplicitNullableDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
|
||||
{
|
||||
if (
|
||||
PHP_VERSION_ID >= 80400
|
||||
&& str_contains($message, 'the explicit nullable type must be used instead')
|
||||
// Only Kint and Faker, which cause this error, are logged.
|
||||
&& (str_starts_with($message, 'Kint\\') || str_starts_with($message, 'Faker\\'))
|
||||
) {
|
||||
log_message(
|
||||
'wranting',
|
||||
'[DEPRECATED] {message} in {errFile} on line {errLine}.',
|
||||
[
|
||||
'message' => $message,
|
||||
'errFile' => clean_path($file ?? ''),
|
||||
'errLine' => $line ?? 0,
|
||||
],
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if any errors have happened during shutdown that
|
||||
* need to be caught and handle them.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function shutdownHandler()
|
||||
{
|
||||
$error = error_get_last();
|
||||
|
||||
if ($error === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error;
|
||||
|
||||
if ($this->exceptionCaughtByExceptionHandler instanceof Throwable) {
|
||||
$message .= "\n【Previous Exception】\n"
|
||||
. $this->exceptionCaughtByExceptionHandler::class . "\n"
|
||||
. $this->exceptionCaughtByExceptionHandler->getMessage() . "\n"
|
||||
. $this->exceptionCaughtByExceptionHandler->getTraceAsString();
|
||||
}
|
||||
|
||||
if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) {
|
||||
$this->exceptionHandler(new ErrorException($message, 0, $type, $file, $line));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the view to display based on the exception thrown,
|
||||
* whether an HTTP or CLI request, etc.
|
||||
*
|
||||
* @return string The path and filename of the view file to use
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to ExceptionHandler.
|
||||
*/
|
||||
protected function determineView(Throwable $exception, string $templatePath): string
|
||||
{
|
||||
// Production environments should have a custom exception file.
|
||||
$view = 'production.php';
|
||||
$templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (
|
||||
in_array(
|
||||
strtolower(ini_get('display_errors')),
|
||||
['1', 'true', 'on', 'yes'],
|
||||
true,
|
||||
)
|
||||
) {
|
||||
$view = 'error_exception.php';
|
||||
}
|
||||
|
||||
// 404 Errors
|
||||
if ($exception instanceof PageNotFoundException) {
|
||||
return 'error_404.php';
|
||||
}
|
||||
|
||||
// Allow for custom views based upon the status code
|
||||
if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) {
|
||||
return 'error_' . $exception->getCode() . '.php';
|
||||
}
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an exception and status code will display the error to the client.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
protected function render(Throwable $exception, int $statusCode)
|
||||
{
|
||||
// Determine possible directories of error views
|
||||
$path = $this->viewPath;
|
||||
$altPath = rtrim((new Paths())->viewDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR;
|
||||
|
||||
$path .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
|
||||
$altPath .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Determine the views
|
||||
$view = $this->determineView($exception, $path);
|
||||
$altView = $this->determineView($exception, $altPath);
|
||||
|
||||
// Check if the view exists
|
||||
if (is_file($path . $view)) {
|
||||
$viewFile = $path . $view;
|
||||
} elseif (is_file($altPath . $altView)) {
|
||||
$viewFile = $altPath . $altView;
|
||||
}
|
||||
|
||||
if (! isset($viewFile)) {
|
||||
echo 'The error view files were not found. Cannot render exception trace.';
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo (function () use ($exception, $statusCode, $viewFile): string {
|
||||
$vars = $this->collectVars($exception, $statusCode);
|
||||
extract($vars, EXTR_SKIP);
|
||||
|
||||
ob_start();
|
||||
include $viewFile;
|
||||
|
||||
return ob_get_clean();
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the variables that will be made available to the view.
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
protected function collectVars(Throwable $exception, int $statusCode): array
|
||||
{
|
||||
// Get the first exception.
|
||||
$firstException = $exception;
|
||||
|
||||
while ($prevException = $firstException->getPrevious()) {
|
||||
$firstException = $prevException;
|
||||
}
|
||||
|
||||
$trace = $firstException->getTrace();
|
||||
|
||||
if ($this->config->sensitiveDataInTrace !== []) {
|
||||
$trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $exception::class,
|
||||
'type' => $exception::class,
|
||||
'code' => $statusCode,
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $trace,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask sensitive data in the trace.
|
||||
*
|
||||
* @param array $trace
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
protected function maskSensitiveData($trace, array $keysToMask, string $path = '')
|
||||
{
|
||||
foreach ($trace as $i => $line) {
|
||||
$trace[$i]['args'] = $this->maskData($line['args'], $keysToMask);
|
||||
}
|
||||
|
||||
return $trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|object $args
|
||||
*
|
||||
* @return array|object
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
private function maskData($args, array $keysToMask, string $path = '')
|
||||
{
|
||||
foreach ($keysToMask as $keyToMask) {
|
||||
$explode = explode('/', $keyToMask);
|
||||
$index = end($explode);
|
||||
|
||||
if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
|
||||
if (is_array($args) && array_key_exists($index, $args)) {
|
||||
$args[$index] = '******************';
|
||||
} elseif (
|
||||
is_object($args) && property_exists($args, $index)
|
||||
&& isset($args->{$index}) && is_scalar($args->{$index})
|
||||
) {
|
||||
$args->{$index} = '******************';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($args)) {
|
||||
foreach ($args as $pathKey => $subarray) {
|
||||
$args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
|
||||
}
|
||||
} elseif (is_object($args)) {
|
||||
foreach ($args as $pathKey => $subarray) {
|
||||
$args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the HTTP status code and the exit status code for this request.
|
||||
*/
|
||||
protected function determineCodes(Throwable $exception): array
|
||||
{
|
||||
$statusCode = 500;
|
||||
$exitStatus = EXIT_ERROR;
|
||||
|
||||
if ($exception instanceof HTTPExceptionInterface) {
|
||||
$statusCode = $exception->getCode();
|
||||
}
|
||||
|
||||
if ($exception instanceof HasExitCodeInterface) {
|
||||
$exitStatus = $exception->getExitCode();
|
||||
}
|
||||
|
||||
return [$statusCode, $exitStatus];
|
||||
}
|
||||
|
||||
private function isDeprecationError(int $error): bool
|
||||
{
|
||||
$deprecations = E_DEPRECATED | E_USER_DEPRECATED;
|
||||
|
||||
return ($error & $deprecations) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true
|
||||
*/
|
||||
private function handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
|
||||
{
|
||||
// Remove the trace of the error handler.
|
||||
$trace = array_slice(debug_backtrace(), 2);
|
||||
|
||||
log_message(
|
||||
$this->config->deprecationLogLevel,
|
||||
"[DEPRECATED] {message} in {errFile} on line {errLine}.\n{trace}",
|
||||
[
|
||||
'message' => $message,
|
||||
'errFile' => clean_path($file ?? ''),
|
||||
'errLine' => $line ?? 0,
|
||||
'trace' => self::renderBacktrace($trace),
|
||||
],
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Display Methods
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This makes nicer looking paths for the error output.
|
||||
*
|
||||
* @deprecated Use dedicated `clean_path()` function.
|
||||
*/
|
||||
public static function cleanPath(string $file): string
|
||||
{
|
||||
return CI_DEBUG ? $file : match (true) {
|
||||
str_starts_with($file, APPPATH) => 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)),
|
||||
str_starts_with($file, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)),
|
||||
str_starts_with($file, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)),
|
||||
defined('VENDORPATH') && str_starts_with($file, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)),
|
||||
default => $file,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes memory usage in real-world units. Intended for use
|
||||
* with memory_get_usage, etc.
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
public static function describeMemory(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . 'B';
|
||||
}
|
||||
|
||||
if ($bytes < 1_048_576) {
|
||||
return round($bytes / 1024, 2) . 'KB';
|
||||
}
|
||||
|
||||
return round($bytes / 1_048_576, 2) . 'MB';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a syntax-highlighted version of a PHP file.
|
||||
*
|
||||
* @return bool|string
|
||||
*
|
||||
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
|
||||
*/
|
||||
public static function highlightFile(string $file, int $lineNumber, int $lines = 15)
|
||||
{
|
||||
if ($file === '' || ! is_readable($file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set our highlight colors:
|
||||
if (function_exists('ini_set')) {
|
||||
ini_set('highlight.comment', '#767a7e; font-style: italic');
|
||||
ini_set('highlight.default', '#c7c7c7');
|
||||
ini_set('highlight.html', '#06B');
|
||||
ini_set('highlight.keyword', '#f1ce61;');
|
||||
ini_set('highlight.string', '#869d6a');
|
||||
}
|
||||
|
||||
try {
|
||||
$source = file_get_contents($file);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$source = str_replace(["\r\n", "\r"], "\n", $source);
|
||||
$source = explode("\n", highlight_string($source, true));
|
||||
$source = str_replace('<br />', "\n", $source[1]);
|
||||
$source = explode("\n", str_replace("\r\n", "\n", $source));
|
||||
|
||||
// Get just the part to show
|
||||
$start = max($lineNumber - (int) round($lines / 2), 0);
|
||||
|
||||
// Get just the lines we need to display, while keeping line numbers...
|
||||
$source = array_splice($source, $start, $lines, true);
|
||||
|
||||
// Used to format the line number in the source
|
||||
$format = '% ' . strlen((string) ($start + $lines)) . 'd';
|
||||
|
||||
$out = '';
|
||||
// Because the highlighting may have an uneven number
|
||||
// of open and close span tags on one line, we need
|
||||
// to ensure we can close them all to get the lines
|
||||
// showing correctly.
|
||||
$spans = 1;
|
||||
|
||||
foreach ($source as $n => $row) {
|
||||
$spans += substr_count($row, '<span') - substr_count($row, '</span');
|
||||
$row = str_replace(["\r", "\n"], ['', ''], $row);
|
||||
|
||||
if (($n + $start + 1) === $lineNumber) {
|
||||
preg_match_all('#<[^>]+>#', $row, $tags);
|
||||
|
||||
$out .= sprintf(
|
||||
"<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
|
||||
$n + $start + 1,
|
||||
strip_tags($row),
|
||||
implode('', $tags[0]),
|
||||
);
|
||||
} else {
|
||||
$out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($spans > 0) {
|
||||
$out .= str_repeat('</span>', $spans);
|
||||
}
|
||||
|
||||
return '<pre><code>' . $out . '</code></pre>';
|
||||
}
|
||||
|
||||
private static function renderBacktrace(array $backtrace): string
|
||||
{
|
||||
$backtraces = [];
|
||||
|
||||
foreach ($backtrace as $index => $trace) {
|
||||
$frame = $trace + ['file' => '[internal function]', 'line' => '', 'class' => '', 'type' => '', 'args' => []];
|
||||
|
||||
if ($frame['file'] !== '[internal function]') {
|
||||
$frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']);
|
||||
}
|
||||
|
||||
unset($frame['line']);
|
||||
$idx = $index;
|
||||
$idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT);
|
||||
|
||||
$args = implode(', ', array_map(static fn ($value): string => match (true) {
|
||||
is_object($value) => sprintf('Object(%s)', $value::class),
|
||||
is_array($value) => $value !== [] ? '[...]' : '[]',
|
||||
$value === null => 'null',
|
||||
is_resource($value) => sprintf('resource (%s)', get_resource_type($value)),
|
||||
default => var_export($value, true),
|
||||
}, $frame['args']));
|
||||
|
||||
$backtraces[] = sprintf(
|
||||
'%s %s: %s%s%s(%s)',
|
||||
$idx,
|
||||
clean_path($frame['file']),
|
||||
$frame['class'],
|
||||
$frame['type'],
|
||||
$frame['function'],
|
||||
$args,
|
||||
);
|
||||
}
|
||||
|
||||
return implode("\n", $backtraces);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Iterator for debugging.
|
||||
*/
|
||||
class Iterator
|
||||
{
|
||||
/**
|
||||
* Stores the tests that we are to run.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tests = [];
|
||||
|
||||
/**
|
||||
* Stores the results of each of the tests.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $results = [];
|
||||
|
||||
/**
|
||||
* Adds a test to run.
|
||||
*
|
||||
* Tests are simply closures that the user can define any sequence of
|
||||
* things to happen during the test.
|
||||
*
|
||||
* @param Closure(): mixed $closure
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function add(string $name, Closure $closure)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
$this->tests[$name] = $closure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs through all of the tests that have been added, recording
|
||||
* time to execute the desired number of iterations, and the approximate
|
||||
* memory usage used during those iterations.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function run(int $iterations = 1000, bool $output = true)
|
||||
{
|
||||
foreach ($this->tests as $name => $test) {
|
||||
// clear memory before start
|
||||
gc_collect_cycles();
|
||||
|
||||
$start = microtime(true);
|
||||
$startMem = $maxMemory = memory_get_usage(true);
|
||||
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
$result = $test();
|
||||
$maxMemory = max($maxMemory, memory_get_usage(true));
|
||||
|
||||
unset($result);
|
||||
}
|
||||
|
||||
$this->results[$name] = [
|
||||
'time' => microtime(true) - $start,
|
||||
'memory' => $maxMemory - $startMem,
|
||||
'n' => $iterations,
|
||||
];
|
||||
}
|
||||
|
||||
if ($output) {
|
||||
return $this->getReport();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get results.
|
||||
*/
|
||||
public function getReport(): string
|
||||
{
|
||||
if ($this->results === []) {
|
||||
return 'No results to display.';
|
||||
}
|
||||
|
||||
helper('number');
|
||||
|
||||
// Template
|
||||
$tpl = '<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Test</td>
|
||||
<td>Time</td>
|
||||
<td>Memory</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>';
|
||||
|
||||
$rows = '';
|
||||
|
||||
foreach ($this->results as $name => $result) {
|
||||
$memory = number_to_size($result['memory'], 4);
|
||||
|
||||
$rows .= "<tr>
|
||||
<td>{$name}</td>
|
||||
<td>" . number_format($result['time'], 4) . "</td>
|
||||
<td>{$memory}</td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
$tpl = str_replace('{rows}', $rows, $tpl);
|
||||
|
||||
return $tpl . '<br/>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\Exceptions\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class Timer
|
||||
*
|
||||
* Provides a simple way to measure the amount of time
|
||||
* that elapses between two points.
|
||||
*
|
||||
* @see \CodeIgniter\Debug\TimerTest
|
||||
*/
|
||||
class Timer
|
||||
{
|
||||
/**
|
||||
* List of all timers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $timers = [];
|
||||
|
||||
/**
|
||||
* Starts a timer running.
|
||||
*
|
||||
* Multiple calls can be made to this method so that several
|
||||
* execution points can be measured.
|
||||
*
|
||||
* @param string $name The name of this timer.
|
||||
* @param float|null $time Allows user to provide time.
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
public function start(string $name, ?float $time = null)
|
||||
{
|
||||
$this->timers[strtolower($name)] = [
|
||||
'start' => ! empty($time) ? $time : microtime(true),
|
||||
'end' => null,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a running timer.
|
||||
*
|
||||
* If the timer is not stopped before the timers() method is called,
|
||||
* it will be automatically stopped at that point.
|
||||
*
|
||||
* @param string $name The name of this timer.
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
public function stop(string $name)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
if (empty($this->timers[$name])) {
|
||||
throw new RuntimeException('Cannot stop timer: invalid name given.');
|
||||
}
|
||||
|
||||
$this->timers[$name]['end'] = microtime(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of a recorded timer.
|
||||
*
|
||||
* @param string $name The name of the timer.
|
||||
* @param int $decimals Number of decimal places.
|
||||
*
|
||||
* @return float|null Returns null if timer does not exist by that name.
|
||||
* Returns a float representing the number of
|
||||
* seconds elapsed while that timer was running.
|
||||
*/
|
||||
public function getElapsedTime(string $name, int $decimals = 4)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
|
||||
if (empty($this->timers[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timer = $this->timers[$name];
|
||||
|
||||
if (empty($timer['end'])) {
|
||||
$timer['end'] = microtime(true);
|
||||
}
|
||||
|
||||
return (float) number_format($timer['end'] - $timer['start'], $decimals, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of timers, with the duration pre-calculated for you.
|
||||
*
|
||||
* @param int $decimals Number of decimal places
|
||||
*/
|
||||
public function getTimers(int $decimals = 4): array
|
||||
{
|
||||
$timers = $this->timers;
|
||||
|
||||
foreach ($timers as &$timer) {
|
||||
if (empty($timer['end'])) {
|
||||
$timer['end'] = microtime(true);
|
||||
}
|
||||
|
||||
$timer['duration'] = (float) number_format($timer['end'] - $timer['start'], $decimals);
|
||||
}
|
||||
|
||||
return $timers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a timer with the specified name exists.
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return array_key_exists(strtolower($name), $this->timers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes callable and measures its time.
|
||||
* Returns its return value if any.
|
||||
*
|
||||
* @param string $name The name of the timer
|
||||
* @param callable(): mixed $callable callable to be executed
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function record(string $name, callable $callable)
|
||||
{
|
||||
$this->start($name);
|
||||
$returnValue = $callable();
|
||||
$this->stop($name);
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug;
|
||||
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\BaseCollector;
|
||||
use CodeIgniter\Debug\Toolbar\Collectors\Config;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Toolbar as ToolbarConfig;
|
||||
|
||||
/**
|
||||
* Displays a toolbar with bits of stats to aid a developer in debugging.
|
||||
*
|
||||
* Inspiration: http://prophiler.fabfuel.de
|
||||
*/
|
||||
class Toolbar
|
||||
{
|
||||
/**
|
||||
* Toolbar configuration settings.
|
||||
*
|
||||
* @var ToolbarConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Collectors to be used and displayed.
|
||||
*
|
||||
* @var list<BaseCollector>
|
||||
*/
|
||||
protected $collectors = [];
|
||||
|
||||
public function __construct(ToolbarConfig $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
foreach ($config->collectors as $collector) {
|
||||
|
||||
$this->collectors[] = new $collector();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the data required by Debug Bar
|
||||
*
|
||||
* @param float $startTime App start time
|
||||
* @param IncomingRequest $request
|
||||
*
|
||||
* @return string JSON encoded data
|
||||
*/
|
||||
public function run(float $startTime, float $totalTime): string
|
||||
{
|
||||
$data = [];
|
||||
// Data items used within the view.
|
||||
$data['url'] = dr_now_url();
|
||||
$data['method'] = 1;
|
||||
$data['isAJAX'] = 2;
|
||||
$data['startTime'] = $startTime;
|
||||
$data['totalTime'] = $totalTime * 1000;
|
||||
$data['totalMemory'] = number_format(memory_get_peak_usage() / 1024 / 1024, 3);
|
||||
$data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7);
|
||||
$data['segmentCount'] = (int) ceil($data['totalTime'] / $data['segmentDuration']);
|
||||
$data['CI_VERSION'] = FRAME_VERSION;
|
||||
$data['collectors'] = [];
|
||||
|
||||
foreach ($this->collectors as $collector) {
|
||||
$data['collectors'][] = $collector->getAsArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
$data['vars']['response'] = [
|
||||
'headers' => [],
|
||||
];
|
||||
|
||||
|
||||
|
||||
$data['config'] = Config::display();
|
||||
|
||||
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called within the view to display the timeline itself.
|
||||
*/
|
||||
protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
|
||||
{
|
||||
$rows = $this->collectTimelineData($collectors);
|
||||
$styleCount = 0;
|
||||
|
||||
// Use recursive render function
|
||||
return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively renders timeline elements and their children.
|
||||
*/
|
||||
protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
|
||||
{
|
||||
$displayTime = $segmentCount * $segmentDuration;
|
||||
|
||||
$output = '';
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$hasChildren = isset($row['children']) && ! empty($row['children']);
|
||||
$isQuery = isset($row['query']) && ! empty($row['query']);
|
||||
|
||||
// Open controller timeline by default
|
||||
$open = $row['name'] === 'Controller';
|
||||
|
||||
if ($hasChildren || $isQuery) {
|
||||
$output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" data-toggle="childrows" data-child="timeline-' . $styleCount . '">';
|
||||
} else {
|
||||
$output .= '<tr>';
|
||||
}
|
||||
|
||||
$output .= '<td class="' . ($isChild ? 'debug-bar-width30' : '') . ' debug-bar-level-' . $level . '" >' . ($hasChildren || $isQuery ? '<nav></nav>' : '') . $row['name'] . '</td>';
|
||||
$output .= '<td class="' . ($isChild ? 'debug-bar-width10' : '') . '">' . $row['component'] . '</td>';
|
||||
$output .= '<td class="' . ($isChild ? 'debug-bar-width10 ' : '') . 'debug-bar-alignRight">' . number_format($row['duration'] * 1000, 2) . ' ms</td>';
|
||||
$output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";
|
||||
|
||||
$offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
|
||||
$length = (((float) $row['duration'] * 1000) / $displayTime) * 100;
|
||||
|
||||
$styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
|
||||
|
||||
$output .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length, 2) . "%'></span>";
|
||||
$output .= '</td>';
|
||||
$output .= '</tr>';
|
||||
|
||||
$styleCount++;
|
||||
|
||||
// Add children if any
|
||||
if ($hasChildren || $isQuery) {
|
||||
$output .= '<tr class="child-row ' . ($open ? '' : ' debug-bar-ndisplay') . '" id="timeline-' . ($styleCount - 1) . '_children" >';
|
||||
$output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
|
||||
$output .= '<table class="timeline">';
|
||||
$output .= '<tbody>';
|
||||
|
||||
if ($isQuery) {
|
||||
// Output query string if query
|
||||
$output .= '<tr>';
|
||||
$output .= '<td class="query-container debug-bar-level-' . ($level + 1) . '" >' . $row['query'] . '</td>';
|
||||
$output .= '</tr>';
|
||||
} else {
|
||||
// Recursively render children
|
||||
$output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
|
||||
}
|
||||
|
||||
$output .= '</tbody>';
|
||||
$output .= '</table>';
|
||||
$output .= '</td>';
|
||||
$output .= '</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sorted array of timeline data arrays from the collectors.
|
||||
*
|
||||
* @param array $collectors
|
||||
*/
|
||||
protected function collectTimelineData($collectors): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
// Collect it
|
||||
foreach ($collectors as $collector) {
|
||||
if (! $collector['hasTimelineData']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_merge($data, $collector['timelineData']);
|
||||
}
|
||||
|
||||
// Sort it
|
||||
$sortArray = [
|
||||
array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
|
||||
array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
|
||||
&$data,
|
||||
];
|
||||
|
||||
array_multisort(...$sortArray);
|
||||
|
||||
// Add end time to each element
|
||||
array_walk($data, static function (&$row): void {
|
||||
$row['end'] = $row['start'] + $row['duration'];
|
||||
});
|
||||
|
||||
// Group it
|
||||
$data = $this->structureTimelineData($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arranges the already sorted timeline data into a parent => child structure.
|
||||
*/
|
||||
protected function structureTimelineData(array $elements): array
|
||||
{
|
||||
// We define ourselves as the first element of the array
|
||||
$element = array_shift($elements);
|
||||
|
||||
// If we have children behind us, collect and attach them to us
|
||||
while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) {
|
||||
$element['children'][] = array_shift($elements);
|
||||
}
|
||||
|
||||
// Make sure our children know whether they have children, too
|
||||
if (isset($element['children'])) {
|
||||
$element['children'] = $this->structureTimelineData($element['children']);
|
||||
}
|
||||
|
||||
// If we have no younger siblings, we can return
|
||||
if ($elements === []) {
|
||||
return [$element];
|
||||
}
|
||||
|
||||
// Make sure our younger siblings know their relatives, too
|
||||
return array_merge([$element], $this->structureTimelineData($elements));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of data from all of the modules
|
||||
* that should be displayed in the 'Vars' tab.
|
||||
*/
|
||||
protected function collectVarData(): array
|
||||
{
|
||||
if (! ($this->config->collectVarData ?? true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($this->collectors as $collector) {
|
||||
if (! $collector->hasVarData()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_merge($data, $collector->getVarData());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a number to the nearest incremental value.
|
||||
*/
|
||||
protected function roundTo(float $number, int $increments = 5): float
|
||||
{
|
||||
$increments = 1 / $increments;
|
||||
|
||||
return ceil($number * $increments) / $increments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for debugging.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare($app)
|
||||
{
|
||||
|
||||
|
||||
if (IS_POST) {
|
||||
return;
|
||||
} elseif (IS_API) {
|
||||
return;
|
||||
} elseif (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
|
||||
return;
|
||||
}
|
||||
|
||||
//ajax请求
|
||||
|
||||
/**
|
||||
* @var IncomingRequest|null $request
|
||||
*/
|
||||
if (CI_DEBUG && ! is_cli()) {
|
||||
|
||||
|
||||
|
||||
$stats = $app->getPerformanceStats();
|
||||
$data = $this->run( $stats['startTime'], $stats['totalTime']);
|
||||
|
||||
helper('filesystem');
|
||||
|
||||
// Updated to microtime() so we can get history
|
||||
$time = sprintf('%.6f', Time::now()->format('U.u'));
|
||||
|
||||
if (! is_dir(WRITEPATH . 'debugbar')) {
|
||||
mkdir(WRITEPATH . 'debugbar', 0777);
|
||||
}
|
||||
|
||||
write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+');
|
||||
|
||||
|
||||
$kintScript = file_get_contents($this->config->viewsPath.'script.js');
|
||||
|
||||
$kintScript = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);
|
||||
$kintScript = ($kintScript === '0') ? '' : $kintScript;
|
||||
|
||||
|
||||
$script = PHP_EOL
|
||||
. '<script id="debugbar_loader" '
|
||||
. 'data-time="' . $time . '" '
|
||||
. 'src="' . WEB_DIR . SELF . '?debugbar"></script>'
|
||||
. '<script id="debugbar_dynamic_script"></script>'
|
||||
. '<style id="debugbar_dynamic_style"></style>'
|
||||
. $kintScript
|
||||
. PHP_EOL;
|
||||
|
||||
echo $script;
|
||||
return ;
|
||||
|
||||
if (str_contains((string) $response->getBody(), '<head>')) {
|
||||
$response->setBody(
|
||||
preg_replace(
|
||||
'/<head>/',
|
||||
'<head>' . $script,
|
||||
$response->getBody(),
|
||||
1
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response->appendBody($script);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inject debug toolbar into the response.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function respond()
|
||||
{
|
||||
|
||||
|
||||
$debugbar = isset($_GET['debugbar']) ? 1 : '';
|
||||
$debugbar_time = isset($_GET['debugbar_time']) && $_GET['debugbar_time'] ? $_GET['debugbar_time'] : '';
|
||||
|
||||
// If the request contains '?debugbar then we're
|
||||
// simply returning the loading script
|
||||
if ($debugbar) {
|
||||
header('Content-Type: application/javascript');
|
||||
|
||||
ob_start();
|
||||
include $this->config->viewsPath . 'toolbarloader.js';
|
||||
$output = ob_get_clean();
|
||||
$output = str_replace('{url}', WEB_DIR.SELF, $output);
|
||||
echo $output;
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Otherwise, if it includes ?debugbar_time, then
|
||||
// we should return the entire debugbar.
|
||||
if ($debugbar_time) {
|
||||
helper('security');
|
||||
|
||||
|
||||
$filename = sanitize_filename('debugbar_' . $debugbar_time);
|
||||
$filename = WRITEPATH . 'debugbar/' . $filename . '.json';
|
||||
|
||||
if (is_file($filename)) {
|
||||
// Show the toolbar if it exists
|
||||
echo $this->format(file_get_contents($filename), 'html');
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Filename not found
|
||||
http_response_code(404);
|
||||
|
||||
exit; // Exit here is needed to avoid loading the index page
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format output
|
||||
*/
|
||||
protected function format(string $data, string $format = 'html'): string
|
||||
{
|
||||
$data = json_decode($data, true);
|
||||
|
||||
|
||||
|
||||
$output = '';
|
||||
|
||||
|
||||
$data['styles'] = [];
|
||||
extract($data);
|
||||
ob_start();
|
||||
include $this->config->viewsPath . 'toolbar.tpl.php';
|
||||
$output = ob_get_clean();
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function render($name, $data)
|
||||
{
|
||||
extract($data);
|
||||
ob_start();
|
||||
include $this->config->viewsPath . $name;
|
||||
$output = ob_get_clean();
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
/**
|
||||
* Base Toolbar collector
|
||||
*/
|
||||
class BaseCollector
|
||||
{
|
||||
/**
|
||||
* Whether this collector has data that can
|
||||
* be displayed in the Timeline.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTimeline = false;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* content in a tab or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTabContent = false;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* a label or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasLabel = false;
|
||||
|
||||
/**
|
||||
* Whether this collector has data that
|
||||
* should be shown in the Vars tab.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasVarData = false;
|
||||
|
||||
/**
|
||||
* The 'title' of this Collector.
|
||||
* Used to name things in the toolbar HTML.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = '';
|
||||
|
||||
/**
|
||||
* Gets the Collector's title.
|
||||
*/
|
||||
public function getTitle(bool $safe = false): string
|
||||
{
|
||||
if ($safe) {
|
||||
return str_replace(' ', '-', strtolower($this->title));
|
||||
}
|
||||
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any information that should be shown next to the title.
|
||||
*/
|
||||
public function getTitleDetails(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this collector need it's own tab?
|
||||
*/
|
||||
public function hasTabContent(): bool
|
||||
{
|
||||
return (bool) $this->hasTabContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this collector have a label?
|
||||
*/
|
||||
public function hasLabel(): bool
|
||||
{
|
||||
return (bool) $this->hasLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this collector have information for the timeline?
|
||||
*/
|
||||
public function hasTimelineData(): bool
|
||||
{
|
||||
return (bool) $this->hasTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the data for the timeline, properly formatted,
|
||||
* or returns an empty array.
|
||||
*/
|
||||
public function timelineData(): array
|
||||
{
|
||||
if (! $this->hasTimeline) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->formatTimelineData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this Collector have data that should be shown in the
|
||||
* 'Vars' tab?
|
||||
*/
|
||||
public function hasVarData(): bool
|
||||
{
|
||||
return (bool) $this->hasVarData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of data that should be shown in the 'Vars' tab.
|
||||
* The format is an array of sections, each with their own array
|
||||
* of key/value pairs:
|
||||
*
|
||||
* $data = [
|
||||
* 'section 1' => [
|
||||
* 'foo' => 'bar,
|
||||
* 'bar' => 'baz'
|
||||
* ],
|
||||
* 'section 2' => [
|
||||
* 'foo' => 'bar,
|
||||
* 'bar' => 'baz'
|
||||
* ],
|
||||
* ];
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getVarData()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Child classes should implement this to return the timeline data
|
||||
* formatted for correct usage.
|
||||
*
|
||||
* Timeline data should be formatted into arrays that look like:
|
||||
*
|
||||
* [
|
||||
* 'name' => 'Database::Query',
|
||||
* 'component' => 'Database',
|
||||
* 'start' => 10 // milliseconds
|
||||
* 'duration' => 15 // milliseconds
|
||||
* ]
|
||||
*/
|
||||
protected function formatTimelineData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data of this collector to be formatted in the toolbar
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
public function display()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes nicer looking paths for the error output.
|
||||
*
|
||||
* @deprecated Use the dedicated `clean_path()` function.
|
||||
*/
|
||||
public function cleanPath(string $file): string
|
||||
{
|
||||
return clean_path($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "badge" value for the button.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getBadgeValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this collector have any data collected?
|
||||
*
|
||||
* If not, then the toolbar button won't get shown.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML to display the icon. Should either
|
||||
* be SVG, or a base-64 encoded.
|
||||
*
|
||||
* Recommended dimensions are 24px x 24px
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return settings as an array.
|
||||
*/
|
||||
public function getAsArray(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->getTitle(),
|
||||
'titleSafe' => $this->getTitle(true),
|
||||
'titleDetails' => $this->getTitleDetails(),
|
||||
'display' => $this->display(),
|
||||
'badgeValue' => $this->getBadgeValue(),
|
||||
'isEmpty' => $this->isEmpty(),
|
||||
'hasTabContent' => $this->hasTabContent(),
|
||||
'hasLabel' => $this->hasLabel(),
|
||||
'icon' => $this->icon(),
|
||||
'hasTimelineData' => $this->hasTimelineData(),
|
||||
'timelineData' => $this->timelineData(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
/**
|
||||
* Debug toolbar configuration
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* Return toolbar config values as an array.
|
||||
*/
|
||||
public static function display(): array
|
||||
{
|
||||
|
||||
return [
|
||||
'ciVersion' => FRAME_VERSION,
|
||||
'phpVersion' => PHP_VERSION,
|
||||
'phpSAPI' => PHP_SAPI,
|
||||
'environment' => ENVIRONMENT,
|
||||
'baseURL' => dr_now_url(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
use CodeIgniter\Database\Query;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Toolbar;
|
||||
|
||||
/**
|
||||
* Collector for the Database tab of the Debug Toolbar.
|
||||
*
|
||||
* @see \CodeIgniter\Debug\Toolbar\Collectors\DatabaseTest
|
||||
*/
|
||||
class Database extends BaseCollector
|
||||
{
|
||||
/**
|
||||
* Whether this collector has timeline data.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTimeline = true;
|
||||
|
||||
/**
|
||||
* Whether this collector should display its own tab.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTabContent = true;
|
||||
|
||||
/**
|
||||
* Whether this collector has data for the Vars tab.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasVarData = false;
|
||||
|
||||
/**
|
||||
* The name used to reference this collector in the toolbar.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'Database';
|
||||
|
||||
/**
|
||||
* Array of database connections.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $connections;
|
||||
|
||||
/**
|
||||
* The query instances that have been collected
|
||||
* through the DBQuery Event.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $queries = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method used during Events to collect
|
||||
* data.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function collect(Query $query)
|
||||
{
|
||||
$config = config(Toolbar::class);
|
||||
|
||||
// Provide default in case it's not set
|
||||
$max = $config->maxQueries ?: 100;
|
||||
|
||||
if (count(static::$queries) < $max) {
|
||||
$queryString = $query->getQuery();
|
||||
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
|
||||
if (! is_cli()) {
|
||||
// when called in the browser, the first two trace arrays
|
||||
// are from the DB event trigger, which are unneeded
|
||||
$backtrace = array_slice($backtrace, 2);
|
||||
}
|
||||
|
||||
static::$queries[] = [
|
||||
'query' => $query,
|
||||
'string' => $queryString,
|
||||
'duplicate' => in_array($queryString, array_column(static::$queries, 'string', null), true),
|
||||
'trace' => $backtrace,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns timeline data formatted for the toolbar.
|
||||
*
|
||||
* @return array The formatted data or an empty array.
|
||||
*/
|
||||
protected function formatTimelineData(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($this->connections as $alias => $connection) {
|
||||
// Connection Time
|
||||
$data[] = [
|
||||
'name' => 'Connecting to Database: "' . $alias . '"',
|
||||
'component' => 'Database',
|
||||
'start' => $connection->getConnectStart(),
|
||||
'duration' => $connection->getConnectDuration(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach (static::$queries as $query) {
|
||||
$data[] = [
|
||||
'name' => 'Query',
|
||||
'component' => 'Database',
|
||||
'start' => $query['query']->getStartTime(true),
|
||||
'duration' => $query['query']->getDuration(),
|
||||
'query' => $query['query']->debugToolbarDisplay(),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data of this collector to be formatted in the toolbar
|
||||
*/
|
||||
public function display(): array
|
||||
{
|
||||
$data = [];
|
||||
$data['queries'] = array_map(static function (array $query): array {
|
||||
$isDuplicate = $query['duplicate'] === true;
|
||||
|
||||
$firstNonSystemLine = '';
|
||||
|
||||
foreach ($query['trace'] as $index => &$line) {
|
||||
// simplify file and line
|
||||
if (isset($line['file'])) {
|
||||
$line['file'] = clean_path($line['file']) . ':' . $line['line'];
|
||||
unset($line['line']);
|
||||
} else {
|
||||
$line['file'] = '[internal function]';
|
||||
}
|
||||
|
||||
// find the first trace line that does not originate from `system/`
|
||||
if ($firstNonSystemLine === '' && ! str_contains($line['file'], 'SYSTEMPATH')) {
|
||||
$firstNonSystemLine = $line['file'];
|
||||
}
|
||||
|
||||
// simplify function call
|
||||
if (isset($line['class'])) {
|
||||
$line['function'] = $line['class'] . $line['type'] . $line['function'];
|
||||
unset($line['class'], $line['type']);
|
||||
}
|
||||
|
||||
if (strrpos($line['function'], '{closure}') === false) {
|
||||
$line['function'] .= '()';
|
||||
}
|
||||
|
||||
$line['function'] = str_repeat(chr(0xC2) . chr(0xA0), 8) . $line['function'];
|
||||
|
||||
// add index numbering padded with nonbreaking space
|
||||
$indexPadded = str_pad(sprintf('%d', $index + 1), 3, ' ', STR_PAD_LEFT);
|
||||
$indexPadded = preg_replace('/\s/', chr(0xC2) . chr(0xA0), $indexPadded);
|
||||
|
||||
$line['index'] = $indexPadded . str_repeat(chr(0xC2) . chr(0xA0), 4);
|
||||
}
|
||||
|
||||
return [
|
||||
'hover' => $isDuplicate ? 'This query was called more than once.' : '',
|
||||
'class' => $isDuplicate ? 'duplicate' : '',
|
||||
'duration' => ((float) $query['query']->getDuration(5) * 1000) . ' ms',
|
||||
'sql' => $query['query']->debugToolbarDisplay(),
|
||||
'trace' => $query['trace'],
|
||||
'trace-file' => $firstNonSystemLine,
|
||||
'qid' => md5($query['query'] . Time::now()->format('0.u00 U')),
|
||||
];
|
||||
}, static::$queries);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "badge" value for the button.
|
||||
*/
|
||||
public function getBadgeValue(): int
|
||||
{
|
||||
return count(static::$queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Information to be displayed next to the title.
|
||||
*
|
||||
* @return string The number of queries (in parentheses) or an empty string.
|
||||
*/
|
||||
public function getTitleDetails(): string
|
||||
{
|
||||
$this->getConnections();
|
||||
|
||||
$queryCount = count(static::$queries);
|
||||
$uniqueCount = count(array_filter(static::$queries, function($query) {
|
||||
return $query['duplicate'] === false;
|
||||
}));
|
||||
$connectionCount = count($this->connections);
|
||||
|
||||
return sprintf(
|
||||
'(%d total Quer%s, %d %s unique across %d Connection%s)',
|
||||
$queryCount,
|
||||
$queryCount > 1 ? 'ies' : 'y',
|
||||
$uniqueCount,
|
||||
$uniqueCount > 1 ? 'of them' : '',
|
||||
$connectionCount,
|
||||
$connectionCount > 1 ? 's' : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this collector have any data collected?
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return static::$queries === [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the icon.
|
||||
*
|
||||
* Icon from https://icons8.com - 1em package
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADMSURBVEhLY6A3YExLSwsA4nIycQDIDIhRWEBqamo/UNF/SjDQjF6ocZgAKPkRiFeEhoYyQ4WIBiA9QAuWAPEHqBAmgLqgHcolGQD1V4DMgHIxwbCxYD+QBqcKINseKo6eWrBioPrtQBq/BcgY5ht0cUIYbBg2AJKkRxCNWkDQgtFUNJwtABr+F6igE8olGQD114HMgHIxAVDyAhA/AlpSA8RYUwoeXAPVex5qHCbIyMgwBCkAuQJIY00huDBUz/mUlBQDqHGjgBjAwAAACexpph6oHSQAAAAASUVORK5CYII=';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the connections from the database config
|
||||
*/
|
||||
private function getConnections(): void
|
||||
{
|
||||
$this->connections = \Config\Database::getConnections();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
/**
|
||||
* Files collector
|
||||
*/
|
||||
class Files extends BaseCollector
|
||||
{
|
||||
/**
|
||||
* Whether this collector has data that can
|
||||
* be displayed in the Timeline.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTimeline = false;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* content in a tab or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTabContent = true;
|
||||
|
||||
/**
|
||||
* The 'title' of this Collector.
|
||||
* Used to name things in the toolbar HTML.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'Files';
|
||||
|
||||
/**
|
||||
* Returns any information that should be shown next to the title.
|
||||
*/
|
||||
public function getTitleDetails(): string
|
||||
{
|
||||
return '( ' . count(get_included_files()) . ' )';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data of this collector to be formatted in the toolbar
|
||||
*/
|
||||
public function display(): array
|
||||
{
|
||||
$rawFiles = get_included_files();
|
||||
$coreFiles = [];
|
||||
$userFiles = [];
|
||||
|
||||
foreach ($rawFiles as $file) {
|
||||
$path = clean_path($file);
|
||||
|
||||
if (str_contains($path, 'SYSTEMPATH')) {
|
||||
$coreFiles[] = [
|
||||
'path' => $path,
|
||||
'name' => basename($file),
|
||||
];
|
||||
} else {
|
||||
$userFiles[] = [
|
||||
'path' => $path,
|
||||
'name' => basename($file),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
sort($userFiles);
|
||||
sort($coreFiles);
|
||||
|
||||
return [
|
||||
'coreFiles' => $coreFiles,
|
||||
'userFiles' => $userFiles,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the number of included files as a badge in the tab button.
|
||||
*/
|
||||
public function getBadgeValue(): int
|
||||
{
|
||||
return count(get_included_files());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the icon.
|
||||
*
|
||||
* Icon from https://icons8.com - 1em package
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Routes collector
|
||||
*/
|
||||
class Routes extends BaseCollector
|
||||
{
|
||||
/**
|
||||
* Whether this collector has data that can
|
||||
* be displayed in the Timeline.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTimeline = false;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* content in a tab or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTabContent = true;
|
||||
|
||||
/**
|
||||
* The 'title' of this Collector.
|
||||
* Used to name things in the toolbar HTML.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'Routes';
|
||||
|
||||
/**
|
||||
* Returns the data of this collector to be formatted in the toolbar
|
||||
*
|
||||
* @return array{
|
||||
* matchedRoute: list<array{
|
||||
* directory: string,
|
||||
* controller: string,
|
||||
* method: string,
|
||||
* paramCount: int,
|
||||
* truePCount: int,
|
||||
* params: list<array{
|
||||
* name: string,
|
||||
* value: mixed
|
||||
* }>
|
||||
* }>,
|
||||
* routes: list<array{
|
||||
* method: string,
|
||||
* route: string,
|
||||
* handler: string
|
||||
* }>
|
||||
* }
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function display(): array
|
||||
{
|
||||
|
||||
$file = APPPATH;
|
||||
if ($file == FRAMEPATH) {
|
||||
$file = CMSPATH.'Control';
|
||||
} else {
|
||||
$file.= 'Controllers';
|
||||
}
|
||||
|
||||
if (IS_ADMIN) {
|
||||
$file.= '/Admin';
|
||||
} elseif (IS_MEMBER) {
|
||||
$file.= '/Member';
|
||||
} elseif (IS_API) {
|
||||
$file.= '/Api';
|
||||
}
|
||||
|
||||
return [
|
||||
'matchedRoute' => [
|
||||
'uri' => \Phpcmf\Service::L('Router')->uri(),
|
||||
'url' => dr_now_url(),
|
||||
'app' => APP_DIR ? APP_DIR : '/',
|
||||
'controller' => \Phpcmf\Service::L('Router')->class,
|
||||
'method' => \Phpcmf\Service::L('Router')->method,
|
||||
'file' => $file.'/'.ucfirst(\Phpcmf\Service::L('Router')->class).'.php',
|
||||
],
|
||||
'get' => $_GET,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of all the routes in the system.
|
||||
*/
|
||||
public function getBadgeValue(): int
|
||||
{
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the icon.
|
||||
*
|
||||
* Icon from https://icons8.com - 1em package
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg==';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Debug\Toolbar\Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* Views collector
|
||||
*/
|
||||
class Views extends BaseCollector
|
||||
{
|
||||
/**
|
||||
* Whether this collector has data that can
|
||||
* be displayed in the Timeline.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTimeline = true;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* content in a tab or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasTabContent = false;
|
||||
|
||||
/**
|
||||
* Whether this collector needs to display
|
||||
* a label or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasLabel = true;
|
||||
|
||||
/**
|
||||
* Whether this collector has data that
|
||||
* should be shown in the Vars tab.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasVarData = true;
|
||||
|
||||
/**
|
||||
* The 'title' of this Collector.
|
||||
* Used to name things in the toolbar HTML.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'Views';
|
||||
|
||||
/**
|
||||
* Instance of the shared Renderer service
|
||||
*
|
||||
* @var RendererInterface|null
|
||||
*/
|
||||
protected $viewer;
|
||||
|
||||
/**
|
||||
* Views counter
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $views = [];
|
||||
|
||||
|
||||
/**
|
||||
* 把CI模板类改成PHPCMF模板类用于debug.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->viewer = \Phpcmf\Service::V();
|
||||
$this->hasTabContent = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data of this collector to be formatted in the toolbar
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function display(): array
|
||||
{
|
||||
|
||||
$vars = [];
|
||||
$tpl_var = $this->viewer->get_data();
|
||||
if ($tpl_var) {
|
||||
foreach ($tpl_var as $key => $value) {
|
||||
if (in_array($key, ['member', 'admin'])) {
|
||||
continue;
|
||||
}
|
||||
$vars[] = [
|
||||
'name' => $key,
|
||||
'value' => var_export($value, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'vars' => $vars,
|
||||
'tips' => $this->viewer->get_load_tips(),
|
||||
'times' => [['tpl' => $this->viewer->get_view_time()]],
|
||||
'files' => $this->viewer->get_view_files(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any information that should be shown next to the title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBadgeValue(): int
|
||||
{
|
||||
return dr_count($this->viewer->get_view_files());
|
||||
}
|
||||
|
||||
|
||||
public function setData($data = [], $context = null)
|
||||
{
|
||||
|
||||
if (! empty($context))
|
||||
{
|
||||
foreach ($data as $key => &$value)
|
||||
{
|
||||
if (is_array($value))
|
||||
{
|
||||
foreach ($value as &$obj)
|
||||
{
|
||||
$obj = $this->objectToArray($obj);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $this->objectToArray($value);
|
||||
}
|
||||
|
||||
$this->dataContexts[$key] = $context;
|
||||
}
|
||||
}
|
||||
|
||||
$this->tempData = $this->tempData ?? $this->data;
|
||||
$this->tempData = array_merge($this->tempData, $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function initViewer(): void
|
||||
{
|
||||
$this->viewer = \Phpcmf\Service::V();
|
||||
}
|
||||
|
||||
/**
|
||||
* Child classes should implement this to return the timeline data
|
||||
* formatted for correct usage.
|
||||
*/
|
||||
protected function formatTimelineData(): array
|
||||
{
|
||||
$this->initViewer();
|
||||
|
||||
$data = [];
|
||||
|
||||
$rows = $this->viewer->getPerformanceData();
|
||||
|
||||
foreach ($rows as $info) {
|
||||
$data[] = [
|
||||
'name' => 'View: ' . $info['view'],
|
||||
'component' => 'Views',
|
||||
'start' => $info['start'],
|
||||
'duration' => $info['end'] - $info['start'],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of data that should be shown in the 'Vars' tab.
|
||||
* The format is an array of sections, each with their own array
|
||||
* of key/value pairs:
|
||||
*
|
||||
* $data = [
|
||||
* 'section 1' => [
|
||||
* 'foo' => 'bar,
|
||||
* 'bar' => 'baz'
|
||||
* ],
|
||||
* 'section 2' => [
|
||||
* 'foo' => 'bar,
|
||||
* 'bar' => 'baz'
|
||||
* ],
|
||||
* ];
|
||||
*/
|
||||
public function getVarData(): array
|
||||
{
|
||||
$this->initViewer();
|
||||
|
||||
return [
|
||||
'View Data' => $this->viewer->getData(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the icon.
|
||||
*
|
||||
* Icon from https://icons8.com - 1em package
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII=';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="debug-bar-width6r">Time</th>
|
||||
<th>Query String</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($queries) { foreach ($queries as $t) { ?>
|
||||
<tr class="<?php echo $t['class'];?>" title="<?php echo $t['hover'];?>" data-toggle="<?php echo $t['qid'];?>-trace">
|
||||
<td class="narrow"><?php echo $t['duration'];?></td>
|
||||
<td><?php echo $t['sql'];?></td>
|
||||
<td class="debug-bar-alignRight"><strong><?php echo $t['trace-file'];?></strong></td>
|
||||
</tr>
|
||||
<tr class="muted debug-bar-ndisplay" id="<?php echo $t['qid'];?>-trace">
|
||||
<td></td>
|
||||
<td colspan="2">
|
||||
<?php foreach ($t['trace'] as $tt) { ?>
|
||||
<?php echo $tt['index'];?>
|
||||
<strong><?php echo $tt['file'];?></strong><br/>
|
||||
<?php echo $tt['function'];?><br/><br/>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,17 @@
|
||||
<table>
|
||||
<tbody>
|
||||
<?php foreach ($userFiles as $t) { ?>
|
||||
<tr>
|
||||
<td> <?php echo $t['name'];?></td>
|
||||
<td> <?php echo $t['path'];?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<?php foreach ($coreFiles as $t) { ?>
|
||||
|
||||
<tr class="muted">
|
||||
<td class="debug-bar-width20e"> <?php echo $t['name'];?> </td>
|
||||
<td> <?php echo $t['path'];?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,32 @@
|
||||
<h3>Matched Route</h3>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<?php if ($matchedRoute) { foreach ($matchedRoute as $key => $tt) { ?>
|
||||
<tr>
|
||||
<td width=200><?php echo $key;?></td>
|
||||
<td><?php echo $tt;?></td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<h3>GET</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>key</th>
|
||||
<th>value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($get) { foreach ($get as $key => $tt) { ?>
|
||||
<tr>
|
||||
<td width=200><?php echo $key;?></td>
|
||||
<td><?php echo $tt;?></td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
<?php if ($times) { foreach ($times as $t) { ?>
|
||||
<h3>运行时间:<?php echo $t['tpl'];?>ms</h3>
|
||||
<?php } } ?>
|
||||
|
||||
|
||||
<br>
|
||||
<h2>模板文件</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width=200>模板</th>
|
||||
<th>路径</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($files) { foreach ($files as $t) { ?>
|
||||
<tr>
|
||||
<td><?php echo $t['name'];?></td>
|
||||
<td><?php echo $t['path'];?></td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
<h2>引用提示</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width=200>模板</th>
|
||||
<th>提示</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($tips) { foreach ($tips as $t) { ?>
|
||||
<tr>
|
||||
<td><?php echo $t['name'];?></td>
|
||||
<td><?php echo $t['tips'];?></td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>模板变量</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<?php if ($vars) { foreach ($vars as $t) { ?>
|
||||
<tr>
|
||||
<td width=200><?php echo $t['name'];?></td>
|
||||
<td><pre><?php echo $t['value'];?></pre></td>
|
||||
</tr>
|
||||
<?php } } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,887 @@
|
||||
/**
|
||||
* This file is part of the CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
#debug-icon {
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clear: both;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
#debug-icon a svg {
|
||||
margin: 8px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
#debug-icon.fixed-top {
|
||||
bottom: auto;
|
||||
top: 0;
|
||||
}
|
||||
#debug-icon .debug-bar-ndisplay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.debug-bar-vars {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#debug-bar {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
#debug-bar h1 {
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
#debug-bar h1 svg {
|
||||
width: 16px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
#debug-bar h2 {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
padding: 5px 0 10px 0;
|
||||
}
|
||||
#debug-bar h2 span {
|
||||
font-size: 13px;
|
||||
}
|
||||
#debug-bar h3 {
|
||||
font-size: 12px;
|
||||
font-weight: 200;
|
||||
margin: 0 0 0 10px;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#debug-bar p {
|
||||
font-size: 12px;
|
||||
margin: 0 0 0 15px;
|
||||
padding: 0;
|
||||
}
|
||||
#debug-bar a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#debug-bar a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#debug-bar button {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
cursor: pointer;
|
||||
line-height: 15px;
|
||||
}
|
||||
#debug-bar button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#debug-bar table {
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
margin: 5px 10px 15px 10px;
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
#debug-bar table strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
#debug-bar table th {
|
||||
display: table-cell;
|
||||
font-weight: 600;
|
||||
padding-bottom: 0.7em;
|
||||
text-align: left;
|
||||
}
|
||||
#debug-bar table tr {
|
||||
border: none;
|
||||
}
|
||||
#debug-bar table td {
|
||||
border: none;
|
||||
display: table-cell;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
#debug-bar table td:first-child {
|
||||
max-width: 20%;
|
||||
}
|
||||
#debug-bar table td:first-child.narrow {
|
||||
width: 7em;
|
||||
}
|
||||
#debug-bar td[data-debugbar-route] form {
|
||||
display: none;
|
||||
}
|
||||
#debug-bar td[data-debugbar-route]:hover form {
|
||||
display: block;
|
||||
}
|
||||
#debug-bar td[data-debugbar-route]:hover > div {
|
||||
display: none;
|
||||
}
|
||||
#debug-bar td[data-debugbar-route] input[type=text] {
|
||||
padding: 2px;
|
||||
}
|
||||
#debug-bar .toolbar {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px 0 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 10000;
|
||||
}
|
||||
#debug-bar .toolbar .rotate {
|
||||
animation: toolbar-rotate 9s linear infinite;
|
||||
}
|
||||
@keyframes toolbar-rotate {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
#debug-bar.fixed-top {
|
||||
bottom: auto;
|
||||
top: 0;
|
||||
}
|
||||
#debug-bar.fixed-top .tab {
|
||||
bottom: auto;
|
||||
top: 36px;
|
||||
}
|
||||
#debug-bar #toolbar-position,
|
||||
#debug-bar #toolbar-theme {
|
||||
padding: 0 6px;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
#debug-bar #toolbar-position:hover,
|
||||
#debug-bar #toolbar-theme:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
#debug-bar #debug-bar-link {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#debug-bar .ci-label {
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
}
|
||||
#debug-bar .ci-label:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
#debug-bar .ci-label a {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
letter-spacing: normal;
|
||||
padding: 0 10px;
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
}
|
||||
#debug-bar .ci-label img {
|
||||
margin: 6px 3px 6px 0;
|
||||
width: 16px !important;
|
||||
}
|
||||
#debug-bar .ci-label .badge {
|
||||
border-radius: 12px;
|
||||
-moz-border-radius: 12px;
|
||||
-webkit-border-radius: 12px;
|
||||
display: inline-block;
|
||||
font-size: 75%;
|
||||
font-weight: bold;
|
||||
line-height: 12px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
text-align: center;
|
||||
vertical-align: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#debug-bar .tab {
|
||||
height: fit-content;
|
||||
text-align: left;
|
||||
bottom: 35px;
|
||||
display: none;
|
||||
left: 0;
|
||||
max-height: 62%;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1em 2em;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
#debug-bar .timeline {
|
||||
position: static;
|
||||
display: table;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#debug-bar .timeline th {
|
||||
border-left: 1px solid;
|
||||
font-size: 12px;
|
||||
font-weight: 200;
|
||||
padding: 5px 5px 10px 5px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
#debug-bar .timeline th:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
#debug-bar .timeline td {
|
||||
border-left: 1px solid;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
#debug-bar .timeline td:first-child {
|
||||
border-left: 0;
|
||||
max-width: none;
|
||||
}
|
||||
#debug-bar .timeline td.child-container {
|
||||
padding: 0px;
|
||||
}
|
||||
#debug-bar .timeline td.child-container .timeline {
|
||||
margin: 0px;
|
||||
}
|
||||
#debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) {
|
||||
padding-left: calc(5px + 10px * var(--level));
|
||||
}
|
||||
#debug-bar .timeline .timer {
|
||||
border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
}
|
||||
#debug-bar .timeline .timeline-parent {
|
||||
cursor: pointer;
|
||||
}
|
||||
#debug-bar .timeline .timeline-parent td:first-child nav {
|
||||
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent;
|
||||
background-position: 0 25%;
|
||||
display: inline-block;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
margin-right: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#debug-bar .timeline .timeline-parent-open {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .timeline .timeline-parent-open td:first-child nav {
|
||||
background-position: 0 75%;
|
||||
}
|
||||
#debug-bar .timeline .child-row:hover {
|
||||
background: transparent;
|
||||
}
|
||||
#debug-bar .route-params,
|
||||
#debug-bar .route-params-item {
|
||||
vertical-align: top;
|
||||
}
|
||||
#debug-bar .route-params td:first-child,
|
||||
#debug-bar .route-params-item td:first-child {
|
||||
font-style: italic;
|
||||
padding-left: 1em;
|
||||
text-align: right;
|
||||
}
|
||||
#debug-bar > .debug-bar-dblock {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.debug-view.show-view {
|
||||
border: 1px solid;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.debug-view-path {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
letter-spacing: normal;
|
||||
min-height: 16px;
|
||||
padding: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.show-view .debug-view-path {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
#debug-bar .ci-label img {
|
||||
margin: unset;
|
||||
}
|
||||
.hide-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
#debug-bar table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
margin: 5px 5px 10px 5px;
|
||||
}
|
||||
#debug-bar table td,
|
||||
#debug-bar table th {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
#debug-bar .timeline {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
#debug-bar .toolbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
#debug-icon {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#debug-icon a:active,
|
||||
#debug-icon a:link,
|
||||
#debug-icon a:visited {
|
||||
color: #DD8615;
|
||||
}
|
||||
|
||||
#debug-bar {
|
||||
background-color: #FFFFFF;
|
||||
color: #434343;
|
||||
}
|
||||
#debug-bar h1,
|
||||
#debug-bar h2,
|
||||
#debug-bar h3,
|
||||
#debug-bar p,
|
||||
#debug-bar a,
|
||||
#debug-bar button,
|
||||
#debug-bar table,
|
||||
#debug-bar thead,
|
||||
#debug-bar tr,
|
||||
#debug-bar td,
|
||||
#debug-bar button,
|
||||
#debug-bar .toolbar {
|
||||
background-color: transparent;
|
||||
color: #434343;
|
||||
}
|
||||
#debug-bar button {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
#debug-bar table strong {
|
||||
color: #DD8615;
|
||||
}
|
||||
#debug-bar table tbody tr:hover {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#debug-bar table tbody tr.current {
|
||||
background-color: #FDC894;
|
||||
}
|
||||
#debug-bar table tbody tr.current:hover td {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#debug-bar .toolbar {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#debug-bar .toolbar img {
|
||||
filter: brightness(0) invert(0.4);
|
||||
}
|
||||
#debug-bar.fixed-top .toolbar {
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#debug-bar.fixed-top .tab {
|
||||
box-shadow: 0 1px 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 1px 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 1px 4px #DFDFDF;
|
||||
}
|
||||
#debug-bar .muted {
|
||||
color: #434343;
|
||||
}
|
||||
#debug-bar .muted td {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .muted:hover td {
|
||||
color: #434343;
|
||||
}
|
||||
#debug-bar #toolbar-position,
|
||||
#debug-bar #toolbar-theme {
|
||||
filter: brightness(0) invert(0.6);
|
||||
}
|
||||
#debug-bar .ci-label.active {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .ci-label:hover {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .ci-label .badge {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#debug-bar .tab {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 -1px 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 -1px 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 -1px 4px #DFDFDF;
|
||||
}
|
||||
#debug-bar .timeline th,
|
||||
#debug-bar .timeline td {
|
||||
border-color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .timeline .timer {
|
||||
background-color: #DD8615;
|
||||
}
|
||||
|
||||
.debug-view.show-view {
|
||||
border-color: #DD8615;
|
||||
}
|
||||
|
||||
.debug-view-path {
|
||||
background-color: #FDC894;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#debug-icon {
|
||||
background-color: #252525;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#debug-icon a:active,
|
||||
#debug-icon a:link,
|
||||
#debug-icon a:visited {
|
||||
color: #DD8615;
|
||||
}
|
||||
#debug-bar {
|
||||
background-color: #252525;
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#debug-bar h1,
|
||||
#debug-bar h2,
|
||||
#debug-bar h3,
|
||||
#debug-bar p,
|
||||
#debug-bar a,
|
||||
#debug-bar button,
|
||||
#debug-bar table,
|
||||
#debug-bar thead,
|
||||
#debug-bar tr,
|
||||
#debug-bar td,
|
||||
#debug-bar button,
|
||||
#debug-bar .toolbar {
|
||||
background-color: transparent;
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#debug-bar button {
|
||||
background-color: #252525;
|
||||
}
|
||||
#debug-bar table strong {
|
||||
color: #DD8615;
|
||||
}
|
||||
#debug-bar table tbody tr:hover {
|
||||
background-color: #434343;
|
||||
}
|
||||
#debug-bar table tbody tr.current {
|
||||
background-color: #FDC894;
|
||||
}
|
||||
#debug-bar table tbody tr.current td {
|
||||
color: #252525;
|
||||
}
|
||||
#debug-bar table tbody tr.current:hover td {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#debug-bar .toolbar {
|
||||
background-color: #434343;
|
||||
box-shadow: 0 0 4px #434343;
|
||||
-moz-box-shadow: 0 0 4px #434343;
|
||||
-webkit-box-shadow: 0 0 4px #434343;
|
||||
}
|
||||
#debug-bar .toolbar img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
#debug-bar.fixed-top .toolbar {
|
||||
box-shadow: 0 0 4px #434343;
|
||||
-moz-box-shadow: 0 0 4px #434343;
|
||||
-webkit-box-shadow: 0 0 4px #434343;
|
||||
}
|
||||
#debug-bar.fixed-top .tab {
|
||||
box-shadow: 0 1px 4px #434343;
|
||||
-moz-box-shadow: 0 1px 4px #434343;
|
||||
-webkit-box-shadow: 0 1px 4px #434343;
|
||||
}
|
||||
#debug-bar .muted {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#debug-bar .muted td {
|
||||
color: #434343;
|
||||
}
|
||||
#debug-bar .muted:hover td {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#debug-bar #toolbar-position,
|
||||
#debug-bar #toolbar-theme {
|
||||
filter: brightness(0) invert(0.6);
|
||||
}
|
||||
#debug-bar .ci-label.active {
|
||||
background-color: #252525;
|
||||
}
|
||||
#debug-bar .ci-label:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
#debug-bar .ci-label .badge {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#debug-bar .tab {
|
||||
background-color: #252525;
|
||||
box-shadow: 0 -1px 4px #434343;
|
||||
-moz-box-shadow: 0 -1px 4px #434343;
|
||||
-webkit-box-shadow: 0 -1px 4px #434343;
|
||||
}
|
||||
#debug-bar .timeline th,
|
||||
#debug-bar .timeline td {
|
||||
border-color: #434343;
|
||||
}
|
||||
#debug-bar .timeline .timer {
|
||||
background-color: #DD8615;
|
||||
}
|
||||
.debug-view.show-view {
|
||||
border-color: #DD8615;
|
||||
}
|
||||
.debug-view-path {
|
||||
background-color: #FDC894;
|
||||
color: #434343;
|
||||
}
|
||||
}
|
||||
#toolbarContainer.dark #debug-icon {
|
||||
background-color: #252525;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-icon a:active,
|
||||
#toolbarContainer.dark #debug-icon a:link,
|
||||
#toolbarContainer.dark #debug-icon a:visited {
|
||||
color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar {
|
||||
background-color: #252525;
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar h1,
|
||||
#toolbarContainer.dark #debug-bar h2,
|
||||
#toolbarContainer.dark #debug-bar h3,
|
||||
#toolbarContainer.dark #debug-bar p,
|
||||
#toolbarContainer.dark #debug-bar a,
|
||||
#toolbarContainer.dark #debug-bar button,
|
||||
#toolbarContainer.dark #debug-bar table,
|
||||
#toolbarContainer.dark #debug-bar thead,
|
||||
#toolbarContainer.dark #debug-bar tr,
|
||||
#toolbarContainer.dark #debug-bar td,
|
||||
#toolbarContainer.dark #debug-bar button,
|
||||
#toolbarContainer.dark #debug-bar .toolbar {
|
||||
background-color: transparent;
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar button {
|
||||
background-color: #252525;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar table strong {
|
||||
color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar table tbody tr:hover {
|
||||
background-color: #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar table tbody tr.current {
|
||||
background-color: #FDC894;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar table tbody tr.current td {
|
||||
color: #252525;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar table tbody tr.current:hover td {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .toolbar {
|
||||
background-color: #434343;
|
||||
box-shadow: 0 0 4px #434343;
|
||||
-moz-box-shadow: 0 0 4px #434343;
|
||||
-webkit-box-shadow: 0 0 4px #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .toolbar img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar.fixed-top .toolbar {
|
||||
box-shadow: 0 0 4px #434343;
|
||||
-moz-box-shadow: 0 0 4px #434343;
|
||||
-webkit-box-shadow: 0 0 4px #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar.fixed-top .tab {
|
||||
box-shadow: 0 1px 4px #434343;
|
||||
-moz-box-shadow: 0 1px 4px #434343;
|
||||
-webkit-box-shadow: 0 1px 4px #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .muted {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .muted td {
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .muted:hover td {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar #toolbar-position,
|
||||
#toolbarContainer.dark #debug-bar #toolbar-theme {
|
||||
filter: brightness(0) invert(0.6);
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .ci-label.active {
|
||||
background-color: #252525;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .ci-label:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .ci-label .badge {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .tab {
|
||||
background-color: #252525;
|
||||
box-shadow: 0 -1px 4px #434343;
|
||||
-moz-box-shadow: 0 -1px 4px #434343;
|
||||
-webkit-box-shadow: 0 -1px 4px #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .timeline th,
|
||||
#toolbarContainer.dark #debug-bar .timeline td {
|
||||
border-color: #434343;
|
||||
}
|
||||
#toolbarContainer.dark #debug-bar .timeline .timer {
|
||||
background-color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.dark .debug-view.show-view {
|
||||
border-color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.dark .debug-view-path {
|
||||
background-color: #FDC894;
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.dark td[data-debugbar-route] input[type=text] {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#toolbarContainer.light #debug-icon {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-icon a:active,
|
||||
#toolbarContainer.light #debug-icon a:link,
|
||||
#toolbarContainer.light #debug-icon a:visited {
|
||||
color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar {
|
||||
background-color: #FFFFFF;
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar h1,
|
||||
#toolbarContainer.light #debug-bar h2,
|
||||
#toolbarContainer.light #debug-bar h3,
|
||||
#toolbarContainer.light #debug-bar p,
|
||||
#toolbarContainer.light #debug-bar a,
|
||||
#toolbarContainer.light #debug-bar button,
|
||||
#toolbarContainer.light #debug-bar table,
|
||||
#toolbarContainer.light #debug-bar thead,
|
||||
#toolbarContainer.light #debug-bar tr,
|
||||
#toolbarContainer.light #debug-bar td,
|
||||
#toolbarContainer.light #debug-bar button,
|
||||
#toolbarContainer.light #debug-bar .toolbar {
|
||||
background-color: transparent;
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar button {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar table strong {
|
||||
color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar table tbody tr:hover {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar table tbody tr.current {
|
||||
background-color: #FDC894;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar table tbody tr.current:hover td {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .toolbar {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .toolbar img {
|
||||
filter: brightness(0) invert(0.4);
|
||||
}
|
||||
#toolbarContainer.light #debug-bar.fixed-top .toolbar {
|
||||
box-shadow: 0 0 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 0 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 0 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar.fixed-top .tab {
|
||||
box-shadow: 0 1px 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 1px 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 1px 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .muted {
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .muted td {
|
||||
color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .muted:hover td {
|
||||
color: #434343;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar #toolbar-position,
|
||||
#toolbarContainer.light #debug-bar #toolbar-theme {
|
||||
filter: brightness(0) invert(0.6);
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .ci-label.active {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .ci-label:hover {
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .ci-label .badge {
|
||||
background-color: #DD4814;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .tab {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 -1px 4px #DFDFDF;
|
||||
-moz-box-shadow: 0 -1px 4px #DFDFDF;
|
||||
-webkit-box-shadow: 0 -1px 4px #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .timeline th,
|
||||
#toolbarContainer.light #debug-bar .timeline td {
|
||||
border-color: #DFDFDF;
|
||||
}
|
||||
#toolbarContainer.light #debug-bar .timeline .timer {
|
||||
background-color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.light .debug-view.show-view {
|
||||
border-color: #DD8615;
|
||||
}
|
||||
#toolbarContainer.light .debug-view-path {
|
||||
background-color: #FDC894;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
.debug-bar-width30 {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.debug-bar-width10 {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.debug-bar-width70p {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.debug-bar-width190p {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.debug-bar-width20e {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.debug-bar-width6r {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.debug-bar-ndisplay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.debug-bar-alignRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.debug-bar-alignLeft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.debug-bar-noverflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-bar-dtableRow {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.debug-bar-dinlineBlock {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.debug-bar-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-bar-mleft4 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.debug-bar-level-0 {
|
||||
--level: 0;
|
||||
}
|
||||
|
||||
.debug-bar-level-1 {
|
||||
--level: 1;
|
||||
}
|
||||
|
||||
.debug-bar-level-2 {
|
||||
--level: 2;
|
||||
}
|
||||
|
||||
.debug-bar-level-3 {
|
||||
--level: 3;
|
||||
}
|
||||
|
||||
.debug-bar-level-4 {
|
||||
--level: 4;
|
||||
}
|
||||
|
||||
.debug-bar-level-5 {
|
||||
--level: 5;
|
||||
}
|
||||
|
||||
.debug-bar-level-6 {
|
||||
--level: 6;
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
/*
|
||||
* Functionality for the CodeIgniter Debug Toolbar.
|
||||
*/
|
||||
|
||||
var ciDebugBar = {
|
||||
toolbarContainer: null,
|
||||
toolbar: null,
|
||||
icon: null,
|
||||
|
||||
init: function () {
|
||||
this.toolbarContainer = document.getElementById("toolbarContainer");
|
||||
this.toolbar = document.getElementById("debug-bar");
|
||||
this.icon = document.getElementById("debug-icon");
|
||||
|
||||
ciDebugBar.createListeners();
|
||||
ciDebugBar.setToolbarState();
|
||||
ciDebugBar.setToolbarPosition();
|
||||
ciDebugBar.setToolbarTheme();
|
||||
ciDebugBar.toggleViewsHints();
|
||||
ciDebugBar.routerLink();
|
||||
ciDebugBar.setHotReloadState();
|
||||
|
||||
document
|
||||
.getElementById("debug-bar-link")
|
||||
.addEventListener("click", ciDebugBar.toggleToolbar, true);
|
||||
document
|
||||
.getElementById("debug-icon-link")
|
||||
.addEventListener("click", ciDebugBar.toggleToolbar, true);
|
||||
|
||||
historyLoad = this.toolbar.getElementsByClassName("ci-history-load");
|
||||
|
||||
if (historyLoad.length) {
|
||||
// Allows highlighting the row of the current history request
|
||||
var btn = this.toolbar.querySelector(
|
||||
'button[data-time="' + localStorage.getItem("debugbar-time-new") + '"]'
|
||||
);
|
||||
ciDebugBar.addClass(btn.parentNode.parentNode, "current");
|
||||
|
||||
|
||||
for (var i = 0; i < historyLoad.length; i++) {
|
||||
historyLoad[i].addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
loadDoc(this.getAttribute("data-time"));
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Display the active Tab on page load
|
||||
var tab = ciDebugBar.readCookie("debug-bar-tab");
|
||||
if (document.getElementById(tab)) {
|
||||
var el = document.getElementById(tab);
|
||||
ciDebugBar.switchClass(el, "debug-bar-ndisplay", "debug-bar-dblock");
|
||||
ciDebugBar.addClass(el, "active");
|
||||
tab = document.querySelector("[data-tab=" + tab + "]");
|
||||
if (tab) {
|
||||
ciDebugBar.addClass(tab.parentNode, "active");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createListeners: function () {
|
||||
var buttons = [].slice.call(
|
||||
this.toolbar.querySelectorAll(".ci-label a")
|
||||
);
|
||||
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].addEventListener("click", ciDebugBar.showTab, true);
|
||||
}
|
||||
|
||||
// Hook up generic toggle via data attributes `data-toggle="foo"`
|
||||
var links = this.toolbar.querySelectorAll("[data-toggle]");
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
let toggleData = links[i].getAttribute("data-toggle");
|
||||
if (toggleData === "datatable") {
|
||||
|
||||
let datatable = links[i].getAttribute("data-table");
|
||||
links[i].addEventListener("click", function() {
|
||||
ciDebugBar.toggleDataTable(datatable)
|
||||
}, true);
|
||||
|
||||
} else if (toggleData === "childrows") {
|
||||
|
||||
let child = links[i].getAttribute("data-child");
|
||||
links[i].addEventListener("click", function() {
|
||||
ciDebugBar.toggleChildRows(child)
|
||||
}, true);
|
||||
|
||||
} else {
|
||||
links[i].addEventListener("click", ciDebugBar.toggleRows, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showTab: function () {
|
||||
// Get the target tab, if any
|
||||
var tab = document.getElementById(this.getAttribute("data-tab"));
|
||||
|
||||
// If the label have not a tab stops here
|
||||
if (! tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove debug-bar-tab cookie
|
||||
ciDebugBar.createCookie("debug-bar-tab", "", -1);
|
||||
|
||||
// Check our current state.
|
||||
var state = tab.classList.contains("debug-bar-dblock");
|
||||
|
||||
// Hide all tabs
|
||||
var tabs = document.querySelectorAll("#debug-bar .tab");
|
||||
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
ciDebugBar.switchClass(tabs[i], "debug-bar-dblock", "debug-bar-ndisplay");
|
||||
}
|
||||
|
||||
// Mark all labels as inactive
|
||||
var labels = document.querySelectorAll("#debug-bar .ci-label");
|
||||
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
ciDebugBar.removeClass(labels[i], "active");
|
||||
}
|
||||
|
||||
// Show/hide the selected tab
|
||||
if (! state) {
|
||||
ciDebugBar.switchClass(tab, "debug-bar-ndisplay", "debug-bar-dblock");
|
||||
ciDebugBar.addClass(this.parentNode, "active");
|
||||
// Create debug-bar-tab cookie to persistent state
|
||||
ciDebugBar.createCookie(
|
||||
"debug-bar-tab",
|
||||
this.getAttribute("data-tab"),
|
||||
365
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
addClass: function (el, className) {
|
||||
if (el.classList) {
|
||||
el.classList.add(className);
|
||||
} else {
|
||||
el.className += " " + className;
|
||||
}
|
||||
},
|
||||
|
||||
removeClass: function (el, className) {
|
||||
if (el.classList) {
|
||||
el.classList.remove(className);
|
||||
} else {
|
||||
el.className = el.className.replace(
|
||||
new RegExp(
|
||||
"(^|\\b)" + className.split(" ").join("|") + "(\\b|$)",
|
||||
"gi"
|
||||
),
|
||||
" "
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
switchClass : function(el, classFrom, classTo) {
|
||||
ciDebugBar.removeClass(el, classFrom);
|
||||
ciDebugBar.addClass(el, classTo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle display of another object based on
|
||||
* the data-toggle value of this object
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
toggleRows: function (event) {
|
||||
if (event.target) {
|
||||
let row = event.target.closest("tr");
|
||||
let target = document.getElementById(
|
||||
row.getAttribute("data-toggle")
|
||||
);
|
||||
|
||||
if (target.classList.contains("debug-bar-ndisplay")) {
|
||||
ciDebugBar.switchClass(target, "debug-bar-ndisplay", "debug-bar-dtableRow");
|
||||
} else {
|
||||
ciDebugBar.switchClass(target, "debug-bar-dtableRow", "debug-bar-ndisplay");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle display of a data table
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
toggleDataTable: function (obj) {
|
||||
if (typeof obj == "string") {
|
||||
obj = document.getElementById(obj + "_table");
|
||||
}
|
||||
|
||||
if (obj) {
|
||||
if (obj.classList.contains("debug-bar-ndisplay")) {
|
||||
ciDebugBar.switchClass(obj, "debug-bar-ndisplay", "debug-bar-dblock");
|
||||
} else {
|
||||
ciDebugBar.switchClass(obj, "debug-bar-dblock", "debug-bar-ndisplay");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle display of timeline child elements
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
toggleChildRows: function (obj) {
|
||||
if (typeof obj == "string") {
|
||||
par = document.getElementById(obj + "_parent");
|
||||
obj = document.getElementById(obj + "_children");
|
||||
}
|
||||
|
||||
if (par && obj) {
|
||||
|
||||
if (obj.classList.contains("debug-bar-ndisplay")) {
|
||||
ciDebugBar.removeClass(obj, "debug-bar-ndisplay");
|
||||
} else {
|
||||
ciDebugBar.addClass(obj, "debug-bar-ndisplay");
|
||||
}
|
||||
|
||||
par.classList.toggle("timeline-parent-open");
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggle tool bar from full to icon and icon to full
|
||||
*/
|
||||
toggleToolbar: function () {
|
||||
var open = ! ciDebugBar.toolbar.classList.contains("debug-bar-ndisplay");
|
||||
|
||||
if (open) {
|
||||
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
|
||||
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
|
||||
} else {
|
||||
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
|
||||
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
|
||||
}
|
||||
|
||||
// Remember it for other page loads on this site
|
||||
ciDebugBar.createCookie("debug-bar-state", "", -1);
|
||||
ciDebugBar.createCookie(
|
||||
"debug-bar-state",
|
||||
open == true ? "minimized" : "open",
|
||||
365
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the initial state of the toolbar (open or minimized) when
|
||||
* the page is first loaded to allow it to remember the state between refreshes.
|
||||
*/
|
||||
setToolbarState: function () {
|
||||
var open = ciDebugBar.readCookie("debug-bar-state");
|
||||
|
||||
if (open != "open") {
|
||||
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
|
||||
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
|
||||
} else {
|
||||
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
|
||||
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
|
||||
}
|
||||
},
|
||||
|
||||
toggleViewsHints: function () {
|
||||
// Avoid toggle hints on history requests that are not the initial
|
||||
if (
|
||||
localStorage.getItem("debugbar-time") !=
|
||||
localStorage.getItem("debugbar-time-new")
|
||||
) {
|
||||
var a = document.querySelector('a[data-tab="ci-views"]');
|
||||
a.href = "#";
|
||||
return;
|
||||
}
|
||||
|
||||
var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ]
|
||||
var sortedComments = [];
|
||||
var comments = [];
|
||||
|
||||
var getComments = function () {
|
||||
var nodes = [];
|
||||
var result = [];
|
||||
var xpathResults = document.evaluate(
|
||||
"//comment()[starts-with(., ' DEBUG-VIEW')]",
|
||||
document,
|
||||
null,
|
||||
XPathResult.ANY_TYPE,
|
||||
null
|
||||
);
|
||||
var nextNode = xpathResults.iterateNext();
|
||||
while (nextNode) {
|
||||
nodes.push(nextNode);
|
||||
nextNode = xpathResults.iterateNext();
|
||||
}
|
||||
|
||||
// sort comment by opening and closing tags
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
// get file path + name to use as key
|
||||
var path = nodes[i].nodeValue.substring(
|
||||
18,
|
||||
nodes[i].nodeValue.length - 1
|
||||
);
|
||||
|
||||
if (nodes[i].nodeValue[12] === "S") {
|
||||
// simple check for start comment
|
||||
// create new entry
|
||||
result[path] = [nodes[i], null];
|
||||
} else if (result[path]) {
|
||||
// add to existing entry
|
||||
result[path][1] = nodes[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// find node that has TargetNode as parentNode
|
||||
var getParentNode = function (node, targetNode) {
|
||||
if (node.parentNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.parentNode !== targetNode) {
|
||||
return getParentNode(node.parentNode, targetNode);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
// define invalid & outer ( also invalid ) elements
|
||||
const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"];
|
||||
const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"];
|
||||
|
||||
var getValidElementInner = function (node, reverse) {
|
||||
// handle invalid tags
|
||||
if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) {
|
||||
for (var i = 0; i < document.body.children.length; ++i) {
|
||||
var index = reverse
|
||||
? document.body.children.length - (i + 1)
|
||||
: i;
|
||||
var element = document.body.children[index];
|
||||
|
||||
// skip invalid tags
|
||||
if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [element, reverse];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// get to next valid element
|
||||
while (
|
||||
node !== null &&
|
||||
INVALID_ELEMENTS.indexOf(node.nodeName) !== -1
|
||||
) {
|
||||
node = reverse
|
||||
? node.previousElementSibling
|
||||
: node.nextElementSibling;
|
||||
}
|
||||
|
||||
// return non array if we couldnt find something
|
||||
if (node === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [node, reverse];
|
||||
};
|
||||
|
||||
// get next valid element ( to be safe to add divs )
|
||||
// @return [ element, skip element ] or null if we couldnt find a valid place
|
||||
var getValidElement = function (nodeElement) {
|
||||
if (nodeElement) {
|
||||
if (nodeElement.nextElementSibling !== null) {
|
||||
return (
|
||||
getValidElementInner(
|
||||
nodeElement.nextElementSibling,
|
||||
false
|
||||
) ||
|
||||
getValidElementInner(
|
||||
nodeElement.previousElementSibling,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
if (nodeElement.previousElementSibling !== null) {
|
||||
return getValidElementInner(
|
||||
nodeElement.previousElementSibling,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// something went wrong! -> element is not in DOM
|
||||
return null;
|
||||
};
|
||||
|
||||
function showHints() {
|
||||
// Had AJAX? Reset view blocks
|
||||
sortedComments = getComments();
|
||||
|
||||
for (var key in sortedComments) {
|
||||
var startElement = getValidElement(sortedComments[key][0]);
|
||||
var endElement = getValidElement(sortedComments[key][1]);
|
||||
|
||||
// skip if we couldnt get a valid element
|
||||
if (startElement === null || endElement === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find element which has same parent as startelement
|
||||
var jointParent = getParentNode(
|
||||
endElement[0],
|
||||
startElement[0].parentNode
|
||||
);
|
||||
if (jointParent === null) {
|
||||
// find element which has same parent as endelement
|
||||
jointParent = getParentNode(
|
||||
startElement[0],
|
||||
endElement[0].parentNode
|
||||
);
|
||||
if (jointParent === null) {
|
||||
// both tries failed
|
||||
continue;
|
||||
} else {
|
||||
startElement[0] = jointParent;
|
||||
}
|
||||
} else {
|
||||
endElement[0] = jointParent;
|
||||
}
|
||||
|
||||
var debugDiv = document.createElement("div"); // holder
|
||||
var debugPath = document.createElement("div"); // path
|
||||
var childArray = startElement[0].parentNode.childNodes; // target child array
|
||||
var parent = startElement[0].parentNode;
|
||||
var start, end;
|
||||
|
||||
// setup container
|
||||
debugDiv.classList.add("debug-view");
|
||||
debugDiv.classList.add("show-view");
|
||||
debugPath.classList.add("debug-view-path");
|
||||
debugPath.innerText = key;
|
||||
debugDiv.appendChild(debugPath);
|
||||
|
||||
// calc distance between them
|
||||
// start
|
||||
for (var i = 0; i < childArray.length; ++i) {
|
||||
// check for comment ( start & end ) -> if its before valid start element
|
||||
if (
|
||||
childArray[i] === sortedComments[key][1] ||
|
||||
childArray[i] === sortedComments[key][0] ||
|
||||
childArray[i] === startElement[0]
|
||||
) {
|
||||
start = i;
|
||||
if (childArray[i] === sortedComments[key][0]) {
|
||||
start++; // increase to skip the start comment
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// adjust if we want to skip the start element
|
||||
if (startElement[1]) {
|
||||
start++;
|
||||
}
|
||||
|
||||
// end
|
||||
for (var i = start; i < childArray.length; ++i) {
|
||||
if (childArray[i] === endElement[0]) {
|
||||
end = i;
|
||||
// dont break to check for end comment after end valid element
|
||||
} else if (childArray[i] === sortedComments[key][1]) {
|
||||
// if we found the end comment, we can break
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// move elements
|
||||
var number = end - start;
|
||||
if (endElement[1]) {
|
||||
number++;
|
||||
}
|
||||
for (var i = 0; i < number; ++i) {
|
||||
if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) {
|
||||
// skip invalid childs that can cause problems if moved
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
debugDiv.appendChild(childArray[start]);
|
||||
}
|
||||
|
||||
// add container to DOM
|
||||
nodeList.push(parent.insertBefore(debugDiv, childArray[start]));
|
||||
}
|
||||
|
||||
ciDebugBar.createCookie("debug-view", "show", 365);
|
||||
ciDebugBar.addClass(btn, "active");
|
||||
}
|
||||
|
||||
function hideHints() {
|
||||
for (var i = 0; i < nodeList.length; ++i) {
|
||||
var index;
|
||||
|
||||
// find index
|
||||
for (
|
||||
var j = 0;
|
||||
j < nodeList[i].parentNode.childNodes.length;
|
||||
++j
|
||||
) {
|
||||
if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) {
|
||||
index = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// move child back
|
||||
while (nodeList[i].childNodes.length !== 1) {
|
||||
nodeList[i].parentNode.insertBefore(
|
||||
nodeList[i].childNodes[1],
|
||||
nodeList[i].parentNode.childNodes[index].nextSibling
|
||||
);
|
||||
index++;
|
||||
}
|
||||
|
||||
nodeList[i].parentNode.removeChild(nodeList[i]);
|
||||
}
|
||||
nodeList.length = 0;
|
||||
|
||||
ciDebugBar.createCookie("debug-view", "", -1);
|
||||
ciDebugBar.removeClass(btn, "active");
|
||||
}
|
||||
|
||||
var btn = document.querySelector("[data-tab=ci-views]");
|
||||
|
||||
// If the Views Collector is inactive stops here
|
||||
if (! btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.parentNode.onclick = function () {
|
||||
if (ciDebugBar.readCookie("debug-view")) {
|
||||
hideHints();
|
||||
} else {
|
||||
showHints();
|
||||
}
|
||||
};
|
||||
|
||||
// Determine Hints state on page load
|
||||
if (ciDebugBar.readCookie("debug-view")) {
|
||||
showHints();
|
||||
}
|
||||
},
|
||||
|
||||
setToolbarPosition: function () {
|
||||
var btnPosition = this.toolbar.querySelector("#toolbar-position");
|
||||
|
||||
if (ciDebugBar.readCookie("debug-bar-position") === "top") {
|
||||
ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
|
||||
ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
|
||||
}
|
||||
|
||||
btnPosition.addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
var position = ciDebugBar.readCookie("debug-bar-position");
|
||||
|
||||
ciDebugBar.createCookie("debug-bar-position", "", -1);
|
||||
|
||||
if (! position || position === "bottom") {
|
||||
ciDebugBar.createCookie("debug-bar-position", "top", 365);
|
||||
ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
|
||||
ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
|
||||
} else {
|
||||
ciDebugBar.createCookie(
|
||||
"debug-bar-position",
|
||||
"bottom",
|
||||
365
|
||||
);
|
||||
ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top");
|
||||
ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top");
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
setToolbarTheme: function () {
|
||||
var btnTheme = this.toolbar.querySelector("#toolbar-theme");
|
||||
var isDarkMode = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
var isLightMode = window.matchMedia(
|
||||
"(prefers-color-scheme: light)"
|
||||
).matches;
|
||||
|
||||
// If a cookie is set with a value, we force the color scheme
|
||||
if (ciDebugBar.readCookie("debug-bar-theme") === "dark") {
|
||||
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light");
|
||||
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark");
|
||||
} else if (ciDebugBar.readCookie("debug-bar-theme") === "light") {
|
||||
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
|
||||
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
|
||||
}
|
||||
|
||||
btnTheme.addEventListener(
|
||||
"click",
|
||||
function () {
|
||||
var theme = ciDebugBar.readCookie("debug-bar-theme");
|
||||
|
||||
if (
|
||||
! theme &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
// If there is no cookie, and "prefers-color-scheme" is set to "dark"
|
||||
// It means that the user wants to switch to light mode
|
||||
ciDebugBar.createCookie("debug-bar-theme", "light", 365);
|
||||
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
|
||||
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
|
||||
} else {
|
||||
if (theme === "dark") {
|
||||
ciDebugBar.createCookie(
|
||||
"debug-bar-theme",
|
||||
"light",
|
||||
365
|
||||
);
|
||||
ciDebugBar.removeClass(
|
||||
ciDebugBar.toolbarContainer,
|
||||
"dark"
|
||||
);
|
||||
ciDebugBar.addClass(
|
||||
ciDebugBar.toolbarContainer,
|
||||
"light"
|
||||
);
|
||||
} else {
|
||||
// In any other cases: if there is no cookie, or the cookie is set to
|
||||
// "light", or the "prefers-color-scheme" is "light"...
|
||||
ciDebugBar.createCookie("debug-bar-theme", "dark", 365);
|
||||
ciDebugBar.removeClass(
|
||||
ciDebugBar.toolbarContainer,
|
||||
"light"
|
||||
);
|
||||
ciDebugBar.addClass(
|
||||
ciDebugBar.toolbarContainer,
|
||||
"dark"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
setHotReloadState: function () {
|
||||
var btn = document.getElementById("debug-hot-reload").parentNode;
|
||||
var btnImg = btn.getElementsByTagName("img")[0];
|
||||
var eventSource;
|
||||
|
||||
// If the Hot Reload Collector is inactive stops here
|
||||
if (! btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.onclick = function () {
|
||||
if (ciDebugBar.readCookie("debug-hot-reload")) {
|
||||
ciDebugBar.createCookie("debug-hot-reload", "", -1);
|
||||
ciDebugBar.removeClass(btn, "active");
|
||||
ciDebugBar.removeClass(btnImg, "rotate");
|
||||
|
||||
// Close the EventSource connection if it exists
|
||||
if (typeof eventSource !== "undefined") {
|
||||
eventSource.close();
|
||||
eventSource = void 0; // Undefine the variable
|
||||
}
|
||||
} else {
|
||||
ciDebugBar.createCookie("debug-hot-reload", "show", 365);
|
||||
ciDebugBar.addClass(btn, "active");
|
||||
ciDebugBar.addClass(btnImg, "rotate");
|
||||
|
||||
eventSource = ciDebugBar.hotReloadConnect();
|
||||
}
|
||||
};
|
||||
|
||||
// Determine Hot Reload state on page load
|
||||
if (ciDebugBar.readCookie("debug-hot-reload")) {
|
||||
ciDebugBar.addClass(btn, "active");
|
||||
ciDebugBar.addClass(btnImg, "rotate");
|
||||
eventSource = ciDebugBar.hotReloadConnect();
|
||||
}
|
||||
},
|
||||
|
||||
hotReloadConnect: function () {
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to create a cookie.
|
||||
*
|
||||
* @param name
|
||||
* @param value
|
||||
* @param days
|
||||
*/
|
||||
createCookie: function (name, value, days) {
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
|
||||
var expires = "; expires=" + date.toGMTString();
|
||||
} else {
|
||||
var expires = "";
|
||||
}
|
||||
|
||||
document.cookie =
|
||||
name + "=" + value + expires + "; path=/; samesite=Lax";
|
||||
},
|
||||
|
||||
readCookie: function (name) {
|
||||
var nameEQ = name + "=";
|
||||
var ca = document.cookie.split(";");
|
||||
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == " ") {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameEQ) == 0) {
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
trimSlash: function (text) {
|
||||
return text.replace(/^\/|\/$/g, "");
|
||||
},
|
||||
|
||||
routerLink: function () {
|
||||
var row, _location;
|
||||
var rowGet = this.toolbar.querySelectorAll(
|
||||
'td[data-debugbar-route="GET"]'
|
||||
);
|
||||
var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/;
|
||||
|
||||
for (var i = 0; i < rowGet.length; i++) {
|
||||
row = rowGet[i];
|
||||
if (!/\/\(.+?\)/.test(rowGet[i].innerText)) {
|
||||
ciDebugBar.addClass(row, "debug-bar-pointer");
|
||||
row.setAttribute(
|
||||
"title",
|
||||
location.origin + "/" + ciDebugBar.trimSlash(row.innerText)
|
||||
);
|
||||
row.addEventListener("click", function (ev) {
|
||||
_location =
|
||||
location.origin +
|
||||
"/" +
|
||||
ciDebugBar.trimSlash(ev.target.innerText);
|
||||
var redirectWindow = window.open(_location, "_blank");
|
||||
redirectWindow.location;
|
||||
});
|
||||
} else {
|
||||
row.innerHTML =
|
||||
"<div>" +
|
||||
row.innerText +
|
||||
"</div>" +
|
||||
'<form data-debugbar-route-tpl="' +
|
||||
ciDebugBar.trimSlash(row.innerText.replace(patt, "?")) +
|
||||
'">' +
|
||||
row.innerText.replace(
|
||||
patt,
|
||||
'<input type="text" placeholder="$1">'
|
||||
) +
|
||||
'<input type="submit" value="Go" class="debug-bar-mleft4">' +
|
||||
"</form>";
|
||||
}
|
||||
}
|
||||
|
||||
rowGet = this.toolbar.querySelectorAll(
|
||||
'td[data-debugbar-route="GET"] form'
|
||||
);
|
||||
for (var i = 0; i < rowGet.length; i++) {
|
||||
row = rowGet[i];
|
||||
|
||||
row.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var inputArray = [],
|
||||
t = 0;
|
||||
var input = event.target.querySelectorAll("input[type=text]");
|
||||
var tpl = event.target.getAttribute("data-debugbar-route-tpl");
|
||||
|
||||
for (var n = 0; n < input.length; n++) {
|
||||
if (input[n].value.length > 0) {
|
||||
inputArray.push(input[n].value);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputArray.length > 0) {
|
||||
_location =
|
||||
location.origin +
|
||||
"/" +
|
||||
tpl.replace(/\?/g, function () {
|
||||
return inputArray[t++];
|
||||
});
|
||||
|
||||
var redirectWindow = window.open(_location, "_blank");
|
||||
redirectWindow.location;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php declare(strict_types=1);
|
||||
use CodeIgniter\Debug\Toolbar;
|
||||
use CodeIgniter\View\Parser;
|
||||
|
||||
/**
|
||||
* @var Toolbar $this
|
||||
* @var int $totalTime
|
||||
* @var int $totalMemory
|
||||
* @var string $url
|
||||
* @var string $method
|
||||
* @var bool $isAJAX
|
||||
* @var int $startTime
|
||||
* @var int $totalTime
|
||||
* @var int $totalMemory
|
||||
* @var float $segmentDuration
|
||||
* @var int $segmentCount
|
||||
* @var string $CI_VERSION
|
||||
* @var array $collectors
|
||||
* @var array $vars
|
||||
* @var array $styles
|
||||
* @var Parser $parser
|
||||
*/
|
||||
?>
|
||||
<style>
|
||||
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . '/toolbar.css')) ?>
|
||||
</style>
|
||||
|
||||
<script id="toolbar_js">
|
||||
var ciSiteURL = "<?= rtrim(site_url(), '/') ?>"
|
||||
<?= file_get_contents(__DIR__ . '/toolbar.js') ?>
|
||||
</script>
|
||||
<div id="debug-icon" class="debug-bar-ndisplay">
|
||||
<a id="debug-icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 155 200"><defs/><path fill="#2890ff" d="M73.7 3.7c2.2 7.9-.7 18.5-7.8 29-1.8 2.6-10.7 12.2-19.7 21.3-23.9 24-33.6 37.1-40.3 54.4-7.9 20.6-7.8 40.8.5 58.2C12.8 180 27.6 193 42.5 198l6 2-3-2.2c-21-15.2-22.9-38.7-4.8-58.8 2.5-2.7 4.8-5 5.1-5 .4 0 .7 2.7.7 6.1 0 5.7.2 6.2 3.7 9.5 3 2.7 4.6 3.4 7.8 3.4 5.6 0 9.9-2.4 11.6-6.5 2.9-6.9 1.6-12-5-20.5-10.5-13.4-11.7-23.3-4.3-34.7l3.1-4.8.7 4.7c1.3 8.2 5.8 12.9 25 25.8 20.9 14.1 30.6 26.1 32.8 40.5 1.1 7.2-.1 16.1-3.1 21.8-2.7 5.3-11.2 14.3-16.5 17.4-2.4 1.4-4.3 2.6-4.3 2.8 0 .2 2.4-.4 5.3-1.4 24.1-8.3 42.7-27.1 48.2-48.6 1.9-7.6 1.9-20.2-.1-28.5-3.5-15.2-14.6-30.5-29.9-41.2l-7-4.9-.6 3.3c-.8 4.8-2.6 7.6-5.9 9.3-4.5 2.3-10.3 1.9-13.8-1-6.7-5.7-7.8-14.6-3.7-30.5 3-11.6 3.2-20.6.5-29.1C88.3 18 80.6 6.3 74.8 2.2 73.1.9 73 1 73.7 3.7z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div id="debug-bar">
|
||||
<div class="toolbar">
|
||||
<span id="toolbar-position">↕</span>
|
||||
<span id="toolbar-theme">🔅</span>
|
||||
<span id="hot-reload-btn" class="ci-label">
|
||||
<a id="debug-hot-reload" title="Toggle Hot Reload">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAABNklEQVR4nN2US04CQRCGv/DaiBxEvYWuBRPDKSCIXsCdcg0ULqTI8xIGN7JwTCU/ScV5tTO64Us6maSq/7+nuqvgkLgHopTl+QAWwBToAg3+wMTzM7YBrihp4jkCToEB8OJyRkCFAB5yDDxVoAd8OpNMOkrcAeMAgz3nzsQ0EqkDayXZqXy5Qugrdy2tGNdKeNWv40xCqGpvJK0YEwXt8ooylMZzUnCh4EkJgzNpmFaMrYLNEgbH0thmGVhSUVrSeE8KLv+7RBMFb0oY3EnDeihGN+WZhmJ7ZlnPtKHB5RvtNwy0d5XWaGgqRmp7a/9QLjRevoDLvOSRM+nnlKumk++0xwZlLhVnEulOhnohTS37vnU1t5M/ho7rPR03/LKW1bxNQep6ETZb5mpGW2/Ak2KpF3oYfAPX9Xpc671kqwAAAABJRU5ErkJggg==" />
|
||||
</a>
|
||||
</span>
|
||||
<span class="ci-label">
|
||||
<a data-tab="ci-timeline">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAD7SURBVEhLY6ArSEtLK09NTbWHcvGC9PR0BaDaQiAdUl9fzwQVxg+AFvwHamqHcnGCpKQkeaDa9yD1UD09UCn8AKaBWJySkmIApFehi0ONwwRQBceBLurAh4FqFoHUAtkrgPgREN+ByYEw1DhMANVEMIhAYQ5U1wtU/wmILwLZRlAp/IBYC8gGw88CaFj3A/FnIL4ETDXGUCnyANSC/UC6HIpnQMXAqQXIvo0khxNDjcMEQEmU9AzDuNI7Lgw1DhOAJIEuhQcRKMcC+e+QNHdDpcgD6BaAANSSQqBcENFlDi6AzQKqgkFlwWhxjVI8o2OgmkFaXI8CTMDAAAAxd1O4FzLMaAAAAABJRU5ErkJggg==">
|
||||
<span class="hide-sm"><?= $totalTime ?> ms <?= $totalMemory ?> MB</span>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<?php foreach ($collectors as $c) : ?>
|
||||
<?php if (! $c['isEmpty'] && ($c['hasTabContent'] || $c['hasLabel'])) : ?>
|
||||
<span class="ci-label">
|
||||
<a data-tab="ci-<?= $c['titleSafe'] ?>">
|
||||
<img src="<?= $c['icon'] ?>">
|
||||
<span class="hide-sm">
|
||||
<?= $c['title'] ?>
|
||||
<?php if ($c['badgeValue'] !== null) : ?>
|
||||
<span class="badge"><?= $c['badgeValue'] ?></span>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
<span class="ci-label">
|
||||
<a data-tab="ci-config">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 155 200"><defs/><path fill="#2890ff" d="M73.7 3.7c2.2 7.9-.7 18.5-7.8 29-1.8 2.6-10.7 12.2-19.7 21.3-23.9 24-33.6 37.1-40.3 54.4-7.9 20.6-7.8 40.8.5 58.2C12.8 180 27.6 193 42.5 198l6 2-3-2.2c-21-15.2-22.9-38.7-4.8-58.8 2.5-2.7 4.8-5 5.1-5 .4 0 .7 2.7.7 6.1 0 5.7.2 6.2 3.7 9.5 3 2.7 4.6 3.4 7.8 3.4 5.6 0 9.9-2.4 11.6-6.5 2.9-6.9 1.6-12-5-20.5-10.5-13.4-11.7-23.3-4.3-34.7l3.1-4.8.7 4.7c1.3 8.2 5.8 12.9 25 25.8 20.9 14.1 30.6 26.1 32.8 40.5 1.1 7.2-.1 16.1-3.1 21.8-2.7 5.3-11.2 14.3-16.5 17.4-2.4 1.4-4.3 2.6-4.3 2.8 0 .2 2.4-.4 5.3-1.4 24.1-8.3 42.7-27.1 48.2-48.6 1.9-7.6 1.9-20.2-.1-28.5-3.5-15.2-14.6-30.5-29.9-41.2l-7-4.9-.6 3.3c-.8 4.8-2.6 7.6-5.9 9.3-4.5 2.3-10.3 1.9-13.8-1-6.7-5.7-7.8-14.6-3.7-30.5 3-11.6 3.2-20.6.5-29.1C88.3 18 80.6 6.3 74.8 2.2 73.1.9 73 1 73.7 3.7z"/></svg>
|
||||
|
||||
</a>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- Open/Close Toggle -->
|
||||
<a id="debug-bar-link" role="button" title="Open/Close">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEPSURBVEhL7ZVLDoJAEEThRuoGDwSEG+jCuFU34s3AK3APP1VDDSGMqI1xx0s6M/2rnlHEaMZElmWrPM+vsDvsYbQ7+us0TReSC2EBrEHxCevRYuppYLXkQpC8sVCuGfTvqSE3hFdFwUGuGfRvqSE35NUAfKZrbQNQm2jrMA+gOK+M+FmhDsRL5voHMA8gFGecq0JOXLWlQg7E7AMIxZnjOiZOEJ82gFCcedUE4gS56QP8yf8ywItz7e+RituKlkkDBoIOH4Nd4HZD4NsGYJ/Abn1xEVOcuZ8f0zc/tHiYmzTAwscBvDIK/veyQ9K/rnewjdF26q0kF1IUxZIFPAVW98x/a+qp8L2M/+HMhETRE6S8TxpZ7KGXAAAAAElFTkSuQmCC">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div id="ci-timeline" class="tab">
|
||||
<table class="timeline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="debug-bar-width30">NAME</th>
|
||||
<th class="debug-bar-width10">COMPONENT</th>
|
||||
<th class="debug-bar-width10">DURATION</th>
|
||||
<?php for ($i = 0; $i < $segmentCount; $i++) : ?>
|
||||
<th><?= $i * $segmentDuration ?> ms</th>
|
||||
<?php endfor ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?= $this->renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Collector-provided Tabs -->
|
||||
<?php foreach ($collectors as $c) : ?>
|
||||
<?php if (! $c['isEmpty']) : ?>
|
||||
<?php if ($c['hasTabContent']) : ?>
|
||||
<div id="ci-<?= $c['titleSafe'] ?>" class="tab">
|
||||
<h2><?= $c['title'] ?> <span><?= $c['titleDetails'] ?></span></h2>
|
||||
|
||||
<?= is_string($c['display']) ? $c['display'] : $this->render("_{$c['titleSafe']}.tpl", $c['display']) ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
|
||||
</div>
|
||||
<style>
|
||||
<?php foreach ($styles as $name => $style): ?>
|
||||
<?= sprintf(".%s { %s }\n", $name, $style) ?>
|
||||
<?php endforeach ?>
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
document.addEventListener('DOMContentLoaded', loadDoc, false);
|
||||
|
||||
function loadDoc(time) {
|
||||
if (isNaN(time)) {
|
||||
time = document.getElementById("debugbar_loader").getAttribute("data-time");
|
||||
localStorage.setItem('debugbar-time', time);
|
||||
}
|
||||
|
||||
localStorage.setItem('debugbar-time-new', time);
|
||||
|
||||
let url = '{url}';
|
||||
let xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
let toolbar = document.getElementById("toolbarContainer");
|
||||
|
||||
if (! toolbar) {
|
||||
toolbar = document.createElement('div');
|
||||
toolbar.setAttribute('id', 'toolbarContainer');
|
||||
document.body.appendChild(toolbar);
|
||||
}
|
||||
|
||||
let responseText = this.responseText;
|
||||
let dynamicStyle = document.getElementById('debugbar_dynamic_style');
|
||||
let dynamicScript = document.getElementById('debugbar_dynamic_script');
|
||||
|
||||
// get the first style block, copy contents to dynamic_style, then remove here
|
||||
let start = responseText.indexOf('>', responseText.indexOf('<style')) + 1;
|
||||
let end = responseText.indexOf('</style>', start);
|
||||
dynamicStyle.innerHTML = responseText.substr(start, end - start);
|
||||
responseText = responseText.substr(end + 8);
|
||||
|
||||
// get the first script after the first style, copy contents to dynamic_script, then remove here
|
||||
start = responseText.indexOf('>', responseText.indexOf('<script')) + 1;
|
||||
end = responseText.indexOf('\<\/script>', start);
|
||||
dynamicScript.innerHTML = responseText.substr(start, end - start);
|
||||
responseText = responseText.substr(end + 9);
|
||||
|
||||
// check for last style block, append contents to dynamic_style, then remove here
|
||||
start = responseText.indexOf('>', responseText.indexOf('<style')) + 1;
|
||||
end = responseText.indexOf('</style>', start);
|
||||
dynamicStyle.innerHTML += responseText.substr(start, end - start);
|
||||
responseText = responseText.substr(0, start - 8);
|
||||
|
||||
toolbar.innerHTML = responseText;
|
||||
|
||||
if (typeof ciDebugBar === 'object') {
|
||||
ciDebugBar.init();
|
||||
}
|
||||
} else if (this.readyState === 4 && this.status === 404) {
|
||||
console.log('CodeIgniter DebugBar: File "WRITEPATH/debugbar/debugbar_' + time + '" not found.');
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open("GET", url + "?debugbar_time=" + time, true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
window.oldXHR = window.ActiveXObject
|
||||
? new ActiveXObject('Microsoft.XMLHTTP')
|
||||
: window.XMLHttpRequest;
|
||||
|
||||
function newXHR() {
|
||||
const realXHR = new window.oldXHR();
|
||||
|
||||
realXHR.addEventListener("readystatechange", function() {
|
||||
// Only success responses and URLs that do not contains "debugbar_time" are tracked
|
||||
if (realXHR.readyState === 4 && realXHR.status.toString()[0] === '2' && realXHR.responseURL.indexOf('debugbar_time') === -1) {
|
||||
if (realXHR.getAllResponseHeaders().indexOf("Debugbar-Time") >= 0) {
|
||||
let debugbarTime = realXHR.getResponseHeader('Debugbar-Time');
|
||||
|
||||
if (debugbarTime) {
|
||||
let h2 = document.querySelector('#ci-history > h2');
|
||||
|
||||
if (h2) {
|
||||
h2.innerHTML = 'History <small>You have new debug data.</small> <button id="ci-history-update">Update</button>';
|
||||
document.querySelector('a[data-tab="ci-history"] > span > .badge').className += ' active';
|
||||
document.getElementById('ci-history-update').addEventListener('click', function () {
|
||||
loadDoc(debugbarTime);
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
return realXHR;
|
||||
}
|
||||
|
||||
window.XMLHttpRequest = newXHR;
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Events;
|
||||
|
||||
|
||||
/**
|
||||
* Events
|
||||
*
|
||||
* @see \CodeIgniter\Events\EventsTest
|
||||
*/
|
||||
class Events
|
||||
{
|
||||
public const PRIORITY_LOW = 200;
|
||||
public const PRIORITY_NORMAL = 100;
|
||||
public const PRIORITY_HIGH = 10;
|
||||
|
||||
/**
|
||||
* The list of listeners.
|
||||
*
|
||||
* @var array<string, array{0: bool, 1: list<int>, 2: list<callable(mixed): mixed>}>
|
||||
*/
|
||||
protected static $listeners = [];
|
||||
|
||||
/**
|
||||
* Flag to let us know if we've read from the Config file(s)
|
||||
* and have all of the defined events.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $initialized = false;
|
||||
|
||||
/**
|
||||
* If true, events will not actually be fired.
|
||||
* Useful during testing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $simulate = false;
|
||||
|
||||
/**
|
||||
* Stores information about the events
|
||||
* for display in the debug toolbar.
|
||||
*
|
||||
* @var list<array{start: float, end: float, event: string}>
|
||||
*/
|
||||
protected static $performanceLog = [];
|
||||
|
||||
/**
|
||||
* A list of found files.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected static $files = [];
|
||||
|
||||
/**
|
||||
* Ensures that we have a events file ready.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize()
|
||||
{
|
||||
// Don't overwrite anything....
|
||||
if (static::$initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$files = [];
|
||||
|
||||
|
||||
|
||||
static::$initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an action to happen on an event. The action can be any sort
|
||||
* of callable:
|
||||
*
|
||||
* Events::on('create', 'myFunction'); // procedural function
|
||||
* Events::on('create', ['myClass', 'myMethod']); // Class::method
|
||||
* Events::on('create', [$myInstance, 'myMethod']); // Method on an existing instance
|
||||
* Events::on('create', function() {}); // Closure
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param callable(mixed): mixed $callback
|
||||
* @param int $priority
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function on($eventName, $callback, $priority = self::PRIORITY_NORMAL)
|
||||
{
|
||||
if (! isset(static::$listeners[$eventName])) {
|
||||
static::$listeners[$eventName] = [
|
||||
true, // If there's only 1 item, it's sorted.
|
||||
[$priority],
|
||||
[$callback],
|
||||
];
|
||||
} else {
|
||||
static::$listeners[$eventName][0] = false; // Not sorted
|
||||
static::$listeners[$eventName][1][] = $priority;
|
||||
static::$listeners[$eventName][2][] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs through all subscribed methods running them one at a time,
|
||||
* until either:
|
||||
* a) All subscribers have finished or
|
||||
* b) a method returns false, at which point execution of subscribers stops.
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param mixed ...$arguments
|
||||
*/
|
||||
public static function trigger($eventName, ...$arguments): bool
|
||||
{
|
||||
// Read in our Config/Events file so that we have them all!
|
||||
if (! static::$initialized) {
|
||||
static::initialize();
|
||||
}
|
||||
|
||||
$listeners = static::listeners($eventName);
|
||||
|
||||
foreach ($listeners as $listener) {
|
||||
$start = microtime(true);
|
||||
|
||||
$result = static::$simulate === false ? $listener(...$arguments) : true;
|
||||
|
||||
if (CI_DEBUG) {
|
||||
static::$performanceLog[] = [
|
||||
'start' => $start,
|
||||
'end' => microtime(true),
|
||||
'event' => $eventName,
|
||||
];
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of listeners for a single event. They are
|
||||
* sorted by priority.
|
||||
*
|
||||
* @param string $eventName
|
||||
*
|
||||
* @return list<callable(mixed): mixed>
|
||||
*/
|
||||
public static function listeners($eventName): array
|
||||
{
|
||||
if (! isset(static::$listeners[$eventName])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// The list is not sorted
|
||||
if (! static::$listeners[$eventName][0]) {
|
||||
// Sort it!
|
||||
array_multisort(static::$listeners[$eventName][1], SORT_NUMERIC, static::$listeners[$eventName][2]);
|
||||
|
||||
// Mark it as sorted already!
|
||||
static::$listeners[$eventName][0] = true;
|
||||
}
|
||||
|
||||
return static::$listeners[$eventName][2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single listener from an event.
|
||||
*
|
||||
* If the listener couldn't be found, returns FALSE, else TRUE if
|
||||
* it was removed.
|
||||
*
|
||||
* @param string $eventName
|
||||
* @param callable(mixed): mixed $listener
|
||||
*/
|
||||
public static function removeListener($eventName, callable $listener): bool
|
||||
{
|
||||
if (! isset(static::$listeners[$eventName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (static::$listeners[$eventName][2] as $index => $check) {
|
||||
if ($check === $listener) {
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners.
|
||||
*
|
||||
* If the event_name is specified, only listeners for that event will be
|
||||
* removed, otherwise all listeners for all events are removed.
|
||||
*
|
||||
* @param string|null $eventName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function removeAllListeners($eventName = null)
|
||||
{
|
||||
if ($eventName !== null) {
|
||||
unset(static::$listeners[$eventName]);
|
||||
} else {
|
||||
static::$listeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the path to the file that routes are read from.
|
||||
*
|
||||
* @param list<string> $files
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setFiles(array $files)
|
||||
{
|
||||
static::$files = $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the files that were found/loaded during this request.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function getFiles()
|
||||
{
|
||||
return static::$files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns simulation on or off. When on, events will not be triggered,
|
||||
* simply logged. Useful during testing when you don't actually want
|
||||
* the tests to run.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function simulate(bool $choice = true)
|
||||
{
|
||||
static::$simulate = $choice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the performance log records.
|
||||
*
|
||||
* @return list<array{start: float, end: float, event: string}>
|
||||
*/
|
||||
public static function getPerformanceLogs()
|
||||
{
|
||||
return static::$performanceLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Error: Critical conditions, like component unavailable, etc.
|
||||
*/
|
||||
class CriticalError extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* This trait provides framework exceptions the ability to pinpoint
|
||||
* accurately where the exception was raised rather than instantiated.
|
||||
*
|
||||
* This is used primarily for factory-instantiated exceptions.
|
||||
*/
|
||||
trait DebugTraceableTrait
|
||||
{
|
||||
/**
|
||||
* Tweaks the exception's constructor to assign the file/line to where
|
||||
* it is actually raised rather than were it is instantiated.
|
||||
*/
|
||||
final public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$trace = $this->getTrace()[0];
|
||||
|
||||
if (isset($trace['class']) && $trace['class'] === static::class) {
|
||||
[
|
||||
'line' => $this->line,
|
||||
'file' => $this->file,
|
||||
] = $trace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Provides a domain-level interface for broad capture
|
||||
* of all framework-related exceptions.
|
||||
*
|
||||
* catch (\CodeIgniter\Exceptions\ExceptionInterface) { ... }
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Class FrameworkException
|
||||
*
|
||||
* A collection of exceptions thrown by the framework
|
||||
* that can only be determined at run time.
|
||||
*/
|
||||
class FrameworkException extends RuntimeException
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forEnabledZlibOutputCompression()
|
||||
{
|
||||
return new static(lang('Core.enabledZlibOutputCompression'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidFile(string $path)
|
||||
{
|
||||
return new static(lang('Core.invalidFile', [$path]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forInvalidDirectory(string $path)
|
||||
{
|
||||
return new static(lang('Core.invalidDirectory', [$path]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forCopyError(string $path)
|
||||
{
|
||||
return new static(lang('Core.copyError', [$path]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*
|
||||
* @deprecated 4.5.0 No longer used.
|
||||
*/
|
||||
public static function forMissingExtension(string $extension)
|
||||
{
|
||||
if (str_contains($extension, 'intl')) {
|
||||
// @codeCoverageIgnoreStart
|
||||
$message = sprintf(
|
||||
'The framework needs the following extension(s) installed and loaded: %s.',
|
||||
$extension,
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
$message = lang('Core.missingExtension', [$extension]);
|
||||
}
|
||||
|
||||
return new static($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forNoHandlers(string $class)
|
||||
{
|
||||
return new static(lang('Core.noHandlers', [$class]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forFabricatorCreateFailed(string $table, string $reason)
|
||||
{
|
||||
return new static(lang('Fabricator.createFailed', [$table, $reason]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Interface for Exceptions that has exception code as HTTP status code.
|
||||
*/
|
||||
interface HTTPExceptionInterface extends ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Interface for Exceptions that has exception code as exit code.
|
||||
*/
|
||||
interface HasExitCodeInterface extends ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* Returns exit status code.
|
||||
*/
|
||||
public function getExitCode(): int;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
class PageNotFoundException extends RuntimeException implements HTTPExceptionInterface
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
/**
|
||||
* HTTP status code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 404;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forPageNotFound(?string $message = null)
|
||||
{
|
||||
return new static($message ?? lang('页面未找到'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forControllerNotFound(string $controller)
|
||||
{
|
||||
return new static(lang('控制器(%s)不存在', $controller));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function forMethodNotFound(string $controller, string $method)
|
||||
{
|
||||
return new static(lang('控制器(%s)的方法(%s)不存在', $controller, $method));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown if an error which can only be found on runtime occurs.
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
CLI::error('ERROR: ' . $code);
|
||||
CLI::write($message);
|
||||
CLI::newLine();
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
// The main Exception
|
||||
CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
|
||||
CLI::write($message);
|
||||
CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green'));
|
||||
CLI::newLine();
|
||||
|
||||
$last = $exception;
|
||||
|
||||
while ($prevException = $last->getPrevious()) {
|
||||
$last = $prevException;
|
||||
|
||||
CLI::write(' Caused by:');
|
||||
CLI::write(' [' . $prevException::class . ']', 'red');
|
||||
CLI::write(' ' . $prevException->getMessage());
|
||||
CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
|
||||
CLI::newLine();
|
||||
}
|
||||
|
||||
// The backtrace
|
||||
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
|
||||
$backtraces = $last->getTrace();
|
||||
|
||||
if ($backtraces) {
|
||||
CLI::write('Backtrace:', 'green');
|
||||
}
|
||||
|
||||
foreach ($backtraces as $i => $error) {
|
||||
$padFile = ' '; // 4 spaces
|
||||
$padClass = ' '; // 7 spaces
|
||||
$c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
|
||||
|
||||
if (isset($error['file'])) {
|
||||
$filepath = clean_path($error['file']) . ':' . $error['line'];
|
||||
|
||||
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
|
||||
} else {
|
||||
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
|
||||
}
|
||||
|
||||
$function = '';
|
||||
|
||||
if (isset($error['class'])) {
|
||||
$type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
|
||||
$function .= $padClass . $error['class'] . $type . $error['function'];
|
||||
} elseif (! isset($error['class']) && isset($error['function'])) {
|
||||
$function .= $padClass . $error['function'];
|
||||
}
|
||||
|
||||
$args = implode(', ', array_map(static fn ($value): string => match (true) {
|
||||
is_object($value) => 'Object(' . $value::class . ')',
|
||||
is_array($value) => $value !== [] ? '[...]' : '[]',
|
||||
$value === null => 'null', // return the lowercased version
|
||||
default => var_export($value, true),
|
||||
}, array_values($error['args'] ?? [])));
|
||||
|
||||
$function .= '(' . $args . ')';
|
||||
|
||||
CLI::write($function);
|
||||
CLI::newLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
// On the CLI, we still want errors in productions
|
||||
// so just use the exception template.
|
||||
include __DIR__ . '/error_exception.php';
|
||||
@@ -0,0 +1,193 @@
|
||||
:root {
|
||||
--main-bg-color: #fff;
|
||||
--main-text-color: #555;
|
||||
--dark-text-color: #222;
|
||||
--light-text-color: #c7c7c7;
|
||||
--brand-primary-color: #2890ff;
|
||||
--light-bg-color: #ededee;
|
||||
--dark-bg-color: #404040;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
background: var(--main-bg-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
color: var(--main-text-color);
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-weight: lighter;
|
||||
font-size: 3rem;
|
||||
color: var(--dark-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
h1.headline {
|
||||
margin-top: 20%;
|
||||
font-size: 5rem;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
p.lead {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 75rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
.header {
|
||||
background: var(--light-bg-color);
|
||||
color: var(--dark-text-color);
|
||||
margin-top: 2.17rem;
|
||||
}
|
||||
.header .container {
|
||||
padding: 1rem;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.header p {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
line-height: 2.5;
|
||||
}
|
||||
.header a {
|
||||
color: var(--brand-primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.header:hover a {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.environment {
|
||||
background: var(--brand-primary-color);
|
||||
color: var(--main-bg-color);
|
||||
text-align: center;
|
||||
padding: calc(4px + 0.2083vw);
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: fixed;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.source {
|
||||
background: #343434;
|
||||
color: var(--light-text-color);
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 5px;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.source span.line {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.source span.line .number {
|
||||
color: #666;
|
||||
}
|
||||
.source .line .highlight {
|
||||
display: block;
|
||||
background: var(--dark-text-color);
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
.source span.highlight .number {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
list-style: none;
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tabs li {
|
||||
display: inline;
|
||||
}
|
||||
.tabs a:link,
|
||||
.tabs a:visited {
|
||||
padding: 0 1rem;
|
||||
line-height: 2.7;
|
||||
text-decoration: none;
|
||||
color: var(--dark-text-color);
|
||||
background: var(--light-bg-color);
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.tabs a:hover {
|
||||
background: var(--light-bg-color);
|
||||
border-color: rgba(0,0,0,0.15);
|
||||
}
|
||||
.tabs a.active {
|
||||
background: var(--main-bg-color);
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.tab-content {
|
||||
background: var(--main-bg-color);
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
}
|
||||
.content {
|
||||
padding: 1rem;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 2rem;
|
||||
display: block;
|
||||
text-align: center;
|
||||
line-height: 3.0;
|
||||
background: #d9edf7;
|
||||
border: 1px solid #bcdff1;
|
||||
border-radius: 5px;
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
td {
|
||||
padding: 0.2rem 0.5rem 0.2rem 0;
|
||||
}
|
||||
tr:hover td {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
td pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.trace a {
|
||||
color: inherit;
|
||||
}
|
||||
.trace table {
|
||||
width: auto;
|
||||
}
|
||||
.trace tr td:first-child {
|
||||
min-width: 5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.trace td {
|
||||
background: var(--light-bg-color);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.trace td pre {
|
||||
margin: 0;
|
||||
}
|
||||
.args {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
var tabLinks = new Array();
|
||||
var contentDivs = new Array();
|
||||
|
||||
function init()
|
||||
{
|
||||
// Grab the tab links and content divs from the page
|
||||
var tabListItems = document.getElementById('tabs').childNodes;
|
||||
console.log(tabListItems);
|
||||
for (var i = 0; i < tabListItems.length; i ++)
|
||||
{
|
||||
if (tabListItems[i].nodeName == "LI")
|
||||
{
|
||||
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
|
||||
var id = getHash(tabLink.getAttribute('href'));
|
||||
tabLinks[id] = tabLink;
|
||||
contentDivs[id] = document.getElementById(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign onclick events to the tab links, and
|
||||
// highlight the first tab
|
||||
var i = 0;
|
||||
|
||||
for (var id in tabLinks)
|
||||
{
|
||||
tabLinks[id].onclick = showTab;
|
||||
tabLinks[id].onfocus = function () {
|
||||
this.blur()
|
||||
};
|
||||
if (i == 0)
|
||||
{
|
||||
tabLinks[id].className = 'active';
|
||||
}
|
||||
i ++;
|
||||
}
|
||||
|
||||
// Hide all content divs except the first
|
||||
var i = 0;
|
||||
|
||||
for (var id in contentDivs)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
console.log(contentDivs[id]);
|
||||
contentDivs[id].className = 'content hide';
|
||||
}
|
||||
i ++;
|
||||
}
|
||||
}
|
||||
|
||||
function showTab()
|
||||
{
|
||||
var selectedId = getHash(this.getAttribute('href'));
|
||||
|
||||
// Highlight the selected tab, and dim all others.
|
||||
// Also show the selected content div, and hide all others.
|
||||
for (var id in contentDivs)
|
||||
{
|
||||
if (id == selectedId)
|
||||
{
|
||||
tabLinks[id].className = 'active';
|
||||
contentDivs[id].className = 'content';
|
||||
}
|
||||
else
|
||||
{
|
||||
tabLinks[id].className = '';
|
||||
contentDivs[id].className = 'content hide';
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the browser following the link
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFirstChildWithTagName(element, tagName)
|
||||
{
|
||||
for (var i = 0; i < element.childNodes.length; i ++)
|
||||
{
|
||||
if (element.childNodes[i].nodeName == tagName)
|
||||
{
|
||||
return element.childNodes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHash(url)
|
||||
{
|
||||
var hashPos = url.lastIndexOf('#');
|
||||
return url.substring(hashPos + 1);
|
||||
}
|
||||
|
||||
function toggle(elem)
|
||||
{
|
||||
elem = document.getElementById(elem);
|
||||
|
||||
if (elem.style && elem.style['display'])
|
||||
{
|
||||
// Only works with the "style" attr
|
||||
var disp = elem.style['display'];
|
||||
}
|
||||
else if (elem.currentStyle)
|
||||
{
|
||||
// For MSIE, naturally
|
||||
var disp = elem.currentStyle['display'];
|
||||
}
|
||||
else if (window.getComputedStyle)
|
||||
{
|
||||
// For most other browsers
|
||||
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
|
||||
}
|
||||
|
||||
// Toggle the state of the "display" style
|
||||
elem.style.display = disp == 'block' ? 'none' : 'block';
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= lang('系统故障') ?></title>
|
||||
|
||||
<style>
|
||||
div.logo {
|
||||
height: 200px;
|
||||
width: 155px;
|
||||
display: inline-block;
|
||||
opacity: 0.08;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
left: 50%;
|
||||
margin-left: -73px;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #777;
|
||||
font-weight: 300;
|
||||
}
|
||||
h1 {
|
||||
font-weight: lighter;
|
||||
letter-spacing: normal;
|
||||
font-size: 3rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: #222;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1024px;
|
||||
margin: 5rem auto;
|
||||
padding: 2rem;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
pre {
|
||||
white-space: normal;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
code {
|
||||
background: #fafafa;
|
||||
border: 1px solid #efefef;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid #efefef;
|
||||
padding: 1em 2em 0 2em;
|
||||
font-size: 85%;
|
||||
color: #999;
|
||||
}
|
||||
a:active,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: #dd4814;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>400</h1>
|
||||
|
||||
<p>
|
||||
<?php if (CI_DEBUG) : ?>
|
||||
<?= nl2br(esc($message)) ?>
|
||||
<?php else : ?>
|
||||
<?= lang('系统故障') ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= lang('页面未找到') ?></title>
|
||||
|
||||
<style>
|
||||
div.logo {
|
||||
height: 200px;
|
||||
width: 155px;
|
||||
display: inline-block;
|
||||
opacity: 0.08;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
left: 50%;
|
||||
margin-left: -73px;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #777;
|
||||
font-weight: 300;
|
||||
}
|
||||
h1 {
|
||||
font-weight: lighter;
|
||||
letter-spacing: normal;
|
||||
font-size: 3rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: #222;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1024px;
|
||||
margin: 5rem auto;
|
||||
padding: 2rem;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
border: 1px solid #efefef;
|
||||
border-radius: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
pre {
|
||||
white-space: normal;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
code {
|
||||
background: #fafafa;
|
||||
border: 1px solid #efefef;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid #efefef;
|
||||
padding: 1em 2em 0 2em;
|
||||
font-size: 85%;
|
||||
color: #999;
|
||||
}
|
||||
a:active,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: #dd4814;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>404</h1>
|
||||
<p>
|
||||
<?php if (CI_DEBUG) : ?>
|
||||
<?= nl2br(esc($message)) ?>
|
||||
<?php else : ?>
|
||||
<?= lang('没有找到此种页面') ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user