Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup\Adapter;
|
||||
|
||||
use Xentral\Components\Database\DatabaseConfig;
|
||||
|
||||
/**
|
||||
* Interface AdapterInterface
|
||||
*
|
||||
* @package Xentral\Components\Backup\Adapter
|
||||
*/
|
||||
interface AdapterInterface
|
||||
{
|
||||
/** @var string STATUS_WORKING */
|
||||
const STATUS_WORKING = 'working';
|
||||
|
||||
/** @var string STATUS_WAIT */
|
||||
const STATUS_WAITING = 'waiting';
|
||||
|
||||
/**
|
||||
* Makes MySQL DUMP
|
||||
*
|
||||
* @param DatabaseConfig $config
|
||||
*
|
||||
* @param string $file
|
||||
*
|
||||
* @param null|string|array $sTable
|
||||
*
|
||||
* @param null|string $where
|
||||
*
|
||||
* @param bool $quickMode Without SET INNODB_STRICT_MODE=0; Advantage quick and space-saving
|
||||
*
|
||||
* @return int PidFile
|
||||
*/
|
||||
public function createDump(DatabaseConfig $config, $file, $sTable = null, $where = null, $quickMode=true);
|
||||
|
||||
/**
|
||||
* Makes Backup or System template recovery
|
||||
*
|
||||
* @param DatabaseConfig $config
|
||||
* @param string $file
|
||||
*
|
||||
* @return int pidFile
|
||||
*/
|
||||
public function restoreDump(DatabaseConfig $config, $file);
|
||||
|
||||
/**
|
||||
* returns the current status
|
||||
*
|
||||
* @param string $pidFile
|
||||
*
|
||||
* @return string|self::STATUS_WORKING|self::STATUS_WAITING
|
||||
*/
|
||||
public function getStatus($pidFile);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup\Adapter;
|
||||
|
||||
use Xentral\Components\Database\DatabaseConfig;
|
||||
|
||||
final class ExecAdapter implements AdapterInterface
|
||||
{
|
||||
|
||||
/** @var DatabaseConfig $config */
|
||||
private $config;
|
||||
|
||||
/** @var int timeout */
|
||||
const TIME_OUT = 3600;
|
||||
|
||||
/**
|
||||
* @param DatabaseConfig $config
|
||||
* @param string $file
|
||||
*
|
||||
* @param null|string|array $tables
|
||||
*
|
||||
* @param null|string $where
|
||||
*
|
||||
* @param bool $quickMode Without SET INNODB_STRICT_MODE=0; Advantage quick and space-saving
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function createDump(DatabaseConfig $config, $file, $tables = null, $where = null, $quickMode = true)
|
||||
{
|
||||
$this->config = $config;
|
||||
$sAsBackup = $this->config->getDatabase();
|
||||
if ($tables !== null) {
|
||||
if (is_array($tables)) {
|
||||
$tables = implode(' ', $tables);
|
||||
}
|
||||
$sAsBackup .= ' --tables ' . $tables;
|
||||
}
|
||||
if ($where !== null) {
|
||||
$sAsBackup .= " --where=\"$where\"";
|
||||
}
|
||||
if ($quickMode !== true) {
|
||||
$cmd = "echo 'SET INNODB_STRICT_MODE=0;' > {$file} && mysqldump --no-tablespaces --extended-insert {$sAsBackup} --no-create-db -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}' >> {$file} && gzip -c {$file} > " . $file . '.gz && rm -f' . $file;
|
||||
} else {
|
||||
$cmd = "mysqldump --no-tablespaces --extended-insert {$sAsBackup} --no-create-db -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}' | gzip > " . $file . '.gz';
|
||||
}
|
||||
$this->execute($cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DatabaseConfig $config
|
||||
* @param string $file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restoreDump(DatabaseConfig $config, $file)
|
||||
{
|
||||
$this->config = $config;
|
||||
$cmd = "gunzip < {$file} | mysql -D{$this->config->getDatabase()} -h{$this->config->getHostname()} -u{$this->config->getUsername()} -p'{$this->config->getPassword()}'";
|
||||
$this->execute($cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $pidFile
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStatus($pidFile)
|
||||
{
|
||||
if (file_exists($pidFile) && ($time = file_get_contents($pidFile)) && (time() - (int)$time) < static::TIME_OUT) {
|
||||
return AdapterInterface::STATUS_WORKING;
|
||||
}
|
||||
|
||||
return AdapterInterface::STATUS_WAITING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cmd
|
||||
*/
|
||||
protected function execute($cmd)
|
||||
{
|
||||
@exec($cmd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup;
|
||||
|
||||
use ApplicationCore;
|
||||
use Xentral\Components\Backup\Exception\BackupException;
|
||||
use Xentral\Components\Backup\Adapter\ExecAdapter;
|
||||
use Xentral\Components\Backup\Logger\BackupLog;
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'DatabaseBackup' => 'onInitDatabaseBackup',
|
||||
'FileBackup' => 'onInitFileBackup',
|
||||
'BackupLog' => 'onInitBackupLogger',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return DatabaseBackup
|
||||
*/
|
||||
public static function onInitDatabaseBackup(ContainerInterface $container)
|
||||
{
|
||||
//@codeCoverageIgnoreStart
|
||||
if (!function_exists('exec')) {
|
||||
throw new BackupException(sprintf('function "%s" is missing!', 'exec'));
|
||||
}
|
||||
//@codeCoverageIgnoreEnd
|
||||
/** @var ApplicationCore $app */
|
||||
$app = $container->get('LegacyApplication');
|
||||
|
||||
return new DatabaseBackup(new ExecAdapter(), $app->erp->getTMP());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return FileBackup
|
||||
*/
|
||||
public static function onInitFileBackup(ContainerInterface $container)
|
||||
{
|
||||
/** @var ApplicationCore $app */
|
||||
$app = $container->get('LegacyApplication');
|
||||
|
||||
return new FileBackup($container->get('BackupLog'), $app->erp->getTMP());
|
||||
}
|
||||
|
||||
public static function onInitBackupLogger(ContainerInterface $container)
|
||||
{
|
||||
/** @var ApplicationCore $app */
|
||||
$app = $container->get('LegacyApplication');
|
||||
|
||||
/** @var string $path */
|
||||
$path = $app->erp->GetRootPath() . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
|
||||
|
||||
return new BackupLog($path, 'status.txt');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup;
|
||||
|
||||
use Xentral\Components\Backup\Adapter\AdapterInterface;
|
||||
use Xentral\Components\Database\DatabaseConfig;
|
||||
use Xentral\Components\Backup\Exception\BackupException;
|
||||
|
||||
final class DatabaseBackup
|
||||
{
|
||||
/**
|
||||
* @var AdapterInterface
|
||||
*/
|
||||
private $adapter;
|
||||
|
||||
/** @var string $tmpPath */
|
||||
private $tmpPath;
|
||||
|
||||
/** @var string lock */
|
||||
const PID_NAME = 'backup.lock';
|
||||
|
||||
/**
|
||||
* DatabaseBackup constructor.
|
||||
*
|
||||
* @param AdapterInterface $adapter
|
||||
*
|
||||
* @param string $tmpPath
|
||||
*/
|
||||
public function __construct(AdapterInterface $adapter, $tmpPath)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
$this->tmpPath = $tmpPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates MySQL Dump
|
||||
*
|
||||
* @param DatabaseConfig $config
|
||||
* @param string $file
|
||||
* @param null|string|array $sTable
|
||||
*
|
||||
* @param null|string $where
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function createDump(DatabaseConfig $config, $file, $sTable = null, $where = null)
|
||||
{
|
||||
$sPidFile = $this->getLockFile();
|
||||
file_put_contents($sPidFile, time());
|
||||
$this->adapter->createDump($config, $file, $sTable, $where);
|
||||
@unlink($sPidFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores Database DUMP
|
||||
*
|
||||
* @param DatabaseConfig $config
|
||||
* @param string $file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restoreDump(DatabaseConfig $config, $file)
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
throw new BackupException(sprintf('Database Dump %s not found!', $file));
|
||||
}
|
||||
$sPidFile = $this->getLockFile();
|
||||
file_put_contents($sPidFile, time());
|
||||
$this->adapter->restoreDump($config, $file);
|
||||
@unlink($sPidFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $metaFile
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMetaInfo($metaFile)
|
||||
{
|
||||
if (!empty($metaFile) && file_exists($metaFile) && ($sMetaEnc = file_get_contents($metaFile))) {
|
||||
return $this->decodeJson(base64_decode($sMetaEnc), true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sJSON
|
||||
* @param bool $bAsHash
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function decodeJson($sJSON, $bAsHash = false)
|
||||
{
|
||||
if (($xData = json_decode($sJSON, $bAsHash)) !== null
|
||||
&& (json_last_error() === JSON_ERROR_NONE)) {
|
||||
return $xData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string|AdapterInterface
|
||||
*/
|
||||
public function getLockStatus()
|
||||
{
|
||||
return $this->adapter->getStatus($this->getLockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getLockFile()
|
||||
{
|
||||
return rtrim($this->tmpPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . static::PID_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tables
|
||||
* @param array $excludeKeys
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function excludeCheckSumTables($tables, $excludeKeys = [])
|
||||
{
|
||||
$default = [
|
||||
'backup',
|
||||
'useronline',
|
||||
'logfile',
|
||||
'cronjob_starter_running',
|
||||
'wiki',
|
||||
'protokoll',
|
||||
'cronjob_log',
|
||||
'module_stat',
|
||||
'checkaltertable',
|
||||
'konfiguration',
|
||||
'permissionhistory',
|
||||
'adapterbox_request_log',
|
||||
'hook',
|
||||
'module_action',
|
||||
'prozessstarter',
|
||||
'sqlcache',
|
||||
'systemhealth',
|
||||
'userkonfiguration',
|
||||
'artikel',
|
||||
'shopimport_amazon_throttling',
|
||||
'lieferschein',
|
||||
'report_column',
|
||||
'report_parameter',
|
||||
'notification_message',
|
||||
'module_stat_detail',
|
||||
];
|
||||
$excludeKeys = array_merge($default, $excludeKeys);
|
||||
foreach ($excludeKeys as $key) {
|
||||
if (!array_key_exists($key, $tables)) {
|
||||
continue;
|
||||
}
|
||||
unset($tables[$key]);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $backupFile
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMetaFileName($backupFile)
|
||||
{
|
||||
$asFile = explode('.', $backupFile);
|
||||
array_pop($asFile);
|
||||
$filename = implode('.', $asFile) . '.meta';
|
||||
|
||||
return str_replace('.backup', 'backup/snapshots', $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDumpMetaData($filePath = null)
|
||||
{
|
||||
return $this->getMetaInfo($this->getMetaFileName($filePath));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class BackupException extends RuntimeException implements BackupExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
/**
|
||||
* Interface BackupExceptionInterface
|
||||
*
|
||||
* @package Xentral\Components\Backup\Exception
|
||||
*/
|
||||
interface BackupExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Xentral\Components\Backup\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
|
||||
class LogException extends RuntimeException implements BackupExceptionInterface
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Backup;
|
||||
|
||||
use PHPUnit\Runner\Exception;
|
||||
use Xentral\Components\Backup\Logger\BackupLog;
|
||||
use Xentral\Components\Backup\Exception\BackupException;
|
||||
use ZipArchive;
|
||||
|
||||
final class FileBackup implements FileBackupInterface
|
||||
{
|
||||
|
||||
/** @var string backup path */
|
||||
private $sUserPath;
|
||||
|
||||
/** @var BackupLog $logger */
|
||||
private $logger;
|
||||
|
||||
/** @var string $cacheTmp */
|
||||
private $cacheTmp;
|
||||
|
||||
/**
|
||||
* @param BackupLog $logger
|
||||
* @param string $cacheTmp
|
||||
*/
|
||||
public function __construct(BackupLog $logger, $cacheTmp)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->cacheTmp = $cacheTmp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getMainPath()
|
||||
{
|
||||
$asPath = explode(DIRECTORY_SEPARATOR, $this->sUserPath);
|
||||
array_pop($asPath);
|
||||
|
||||
return implode(DIRECTORY_SEPARATOR, $asPath) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the full file name path
|
||||
*
|
||||
* @param string $filename
|
||||
* @param bool $bIsSnapshots
|
||||
* @param null $userPath
|
||||
*
|
||||
* @throws BackupException
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalPath($filename, $userPath = null, $bIsSnapshots = true)
|
||||
{
|
||||
if (null !== $userPath) {
|
||||
$this->sUserPath = $userPath;
|
||||
}
|
||||
$path = $this->getMainPath();
|
||||
if ($bIsSnapshots === true) {
|
||||
$path .= FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
if (!file_exists($path) && !@mkdir($path) && !is_dir($path)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
|
||||
throw new BackupException(sprintf('Directory "%s" was not created', $path));
|
||||
}
|
||||
|
||||
return $path . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function tmpDir()
|
||||
{
|
||||
return $this->getMainPath() . 'backup/.backup' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSnapshotsDir()
|
||||
{
|
||||
return $this->getMainPath() . FileBackupInterface::SNAPSHOTS_FOLDER . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
protected function addLock($path)
|
||||
{
|
||||
return $this->logger->write(time(), $path, FileBackupInterface::PID_FILE, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function tryPurgePidFile()
|
||||
{
|
||||
$pidFile = $this->tmpDir() . FileBackupInterface::PID_FILE;
|
||||
|
||||
$time = file_get_contents($pidFile);
|
||||
|
||||
if ((time() - (int)$time > FileBackupInterface::TIME_OUT)) {
|
||||
return unlink($pidFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $userPath
|
||||
*
|
||||
* @throws BackupException
|
||||
* @return string|null
|
||||
*/
|
||||
public function begin($userPath = null)
|
||||
{
|
||||
if (null !== $userPath) {
|
||||
$this->sUserPath = $userPath;
|
||||
}
|
||||
$path = $this->tmpDir();
|
||||
|
||||
if (is_dir($path)) {
|
||||
@exec('rm -rf ' . $path);
|
||||
}
|
||||
|
||||
$backupDir = $this->getMainPath() . 'backup';
|
||||
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'status.txt')) {
|
||||
@unlink($backupDir . DIRECTORY_SEPARATOR . 'status.txt');
|
||||
}
|
||||
|
||||
if (file_exists($backupDir . DIRECTORY_SEPARATOR . 'session.txt')) {
|
||||
@unlink($backupDir . DIRECTORY_SEPARATOR . 'session.txt');
|
||||
}
|
||||
|
||||
if (!file_exists($path) && !@mkdir($path, 0777, true) && !is_dir($path)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $path));
|
||||
throw new BackupException(sprintf('Directory "%s" was not created', $path));
|
||||
}
|
||||
|
||||
if ($this->getLockStatus() === FileBackupInterface::STATUS_WORKING && $this->tryPurgePidFile() === false) {
|
||||
return null;
|
||||
//throw new BackupException(sprintf('Backup is Running'));
|
||||
}
|
||||
|
||||
if ($this->addLock($path) === false) {
|
||||
$this->logger->writePersistent('Failed start backup');
|
||||
throw new BackupException('Failed start backup');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function cleanUp($file)
|
||||
{
|
||||
$path = $this->tmpDir();
|
||||
if ($this->moveDir($path . $file, $this->getLocalPath($file)) === true) {
|
||||
return $this->deleteDir($path);
|
||||
}
|
||||
$this->logger->writePersistent(sprintf('Clean Up of %s failed', $path));
|
||||
throw new BackupException(sprintf('Clean Up of %s failed', $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class_name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function classExists($class_name)
|
||||
{
|
||||
return class_exists($class_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ZipArchive $oZip
|
||||
* @param string $fileName
|
||||
* @param int $flags
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function openZipObject($oZip, $fileName, $flags = 0)
|
||||
{
|
||||
return $oZip->open($fileName, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $oldDir
|
||||
* @param string $newDir
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function moveDir($oldDir, $newDir)
|
||||
{
|
||||
return @rename($oldDir, $newDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDir($dir)
|
||||
{
|
||||
return @mkdir($dir) || is_dir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBackupExtension()
|
||||
{
|
||||
return FileBackupInterface::COMPRESS_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename Zipped file name
|
||||
* @param string $userPath local directory to backup
|
||||
* @param string|null $sMySQLFile MySQL Backup file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createBackup($filename, $userPath, $sMySQLFile = null)
|
||||
{
|
||||
$this->sUserPath = $userPath;
|
||||
$rootPath = realpath($userPath);
|
||||
|
||||
if (!file_exists($userPath)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not found', $userPath));
|
||||
throw new BackupException(sprintf('Directory "%s" was not found', $userPath));
|
||||
}
|
||||
|
||||
$tmpFilename = $this->tmpDir() . $filename;
|
||||
$sMySQLFullPath = $this->tmpDir() . $sMySQLFile;
|
||||
|
||||
if (null !== $sMySQLFile && is_file($sMySQLFullPath) && filesize($sMySQLFullPath) > 1024) {
|
||||
$this->logger->write('Add MySQL file to Zip');
|
||||
exec('cd ' . $rootPath . ' && mv ' . $sMySQLFullPath . ' ' . $sMySQLFile);
|
||||
}
|
||||
|
||||
exec('cd ' . $rootPath . ' && zip -r -9 ' . $tmpFilename . ' * .[^.]* -x "wiki/*"');
|
||||
|
||||
if (null !== $sMySQLFile) {
|
||||
exec('cd ' . $rootPath . ' && rm -f ' . $sMySQLFile);
|
||||
}
|
||||
|
||||
return $this->cleanUp($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dirPath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function deleteDir($dirPath)
|
||||
{
|
||||
if (is_dir($dirPath)) {
|
||||
if (substr($dirPath, strlen($dirPath) - 1, 1) !== '/') {
|
||||
$dirPath .= '/';
|
||||
}
|
||||
$files = glob($dirPath . '*', GLOB_MARK);
|
||||
foreach ($files as $file) {
|
||||
if (is_dir($file)) {
|
||||
$this->deleteDir($file);
|
||||
} else {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($dirPath);
|
||||
}
|
||||
$this->logger->writePersistent(sprintf('Deleted DIR %s failed', $dirPath));
|
||||
throw new BackupException(sprintf('Deleted DIR %s failed', $dirPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $backupFile
|
||||
* @param string $userPath
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function restoreFileSystem($backupFile, $userPath, $options = [])
|
||||
{
|
||||
$default = ['template_file_dir' => null, 'exclude_dir' => ['wiki']];
|
||||
$options = array_merge($default, $options);
|
||||
$templateFileDir = $options['template_file_dir'];
|
||||
$this->sUserPath = $userPath;
|
||||
$bIsSnapshots = null === $templateFileDir;
|
||||
|
||||
$this->sUserPath = null === $templateFileDir ? $userPath : $templateFileDir;
|
||||
|
||||
if (file_exists($file = $this->getLocalPath($backupFile, null, $bIsSnapshots))) {
|
||||
$userDataPath = realpath($userPath);
|
||||
$tmpExtract = $this->tmpDir() . FileBackupInterface::LOCAL_FILES_DIR_NAME . 'tmp';
|
||||
if (!file_exists($tmpExtract) && !@mkdir($tmpExtract) && !is_dir($tmpExtract)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $tmpExtract));
|
||||
throw new BackupException(sprintf('Directory "%s" was not created', $tmpExtract));
|
||||
}
|
||||
|
||||
if (!$this->classExists('ZipArchive')) {
|
||||
$this->logger->writePersistent('Class ZipArchive is missing!');
|
||||
throw new BackupException('Class ZipArchive is missing!');
|
||||
}
|
||||
|
||||
$oZip = new ZipArchive();
|
||||
|
||||
if ($this->openZipObject($oZip, $file, ZipArchive::CHECKCONS) !== true) {
|
||||
$this->logger->writePersistent(sprintf('Failure to open file in "%s"', $file));
|
||||
throw new BackupException(sprintf('Failure to open file in "%s"', $file));
|
||||
}
|
||||
|
||||
$oZip->extractTo($tmpExtract);
|
||||
$oZip->close();
|
||||
|
||||
// move user data
|
||||
$shortTmp = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.rmTmp';
|
||||
$sBeforeTmp = $shortTmp . uniqid('', true) . 'before';
|
||||
|
||||
if (!$this->isDir($sBeforeTmp)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $sBeforeTmp));
|
||||
throw new BackupException(sprintf('Directory "%s" was not created', $sBeforeTmp));
|
||||
}
|
||||
|
||||
$this->logger->write('Moving userdata away');
|
||||
|
||||
if (!$this->moveDir($userDataPath, $sBeforeTmp)) {
|
||||
$this->logger->writePersistent(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
|
||||
throw new BackupException(sprintf('Moving %s into %s failed! ', $userDataPath, $sBeforeTmp));
|
||||
}
|
||||
|
||||
if (array_key_exists('exclude_dir', $options) && is_array($options['exclude_dir'])) {
|
||||
$this->excludeDirectory($options['exclude_dir'], $tmpExtract, $sBeforeTmp);
|
||||
}
|
||||
|
||||
$this->logger->write('Recovering userData');
|
||||
if (!$this->moveDir($tmpExtract, $userDataPath)) {
|
||||
$this->logger->writePersistent(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
|
||||
throw new BackupException(sprintf('Moving %s into %s failed!', $tmpExtract, $userDataPath));
|
||||
}
|
||||
|
||||
// FIX TMP ISSUE
|
||||
if (!empty($this->cacheTmp) && is_dir($this->cacheTmp)) {
|
||||
$this->logger->write('Delete DB tmp');
|
||||
$this->deleteDir($this->cacheTmp);
|
||||
}
|
||||
|
||||
// remove DB if exists
|
||||
$backupFileExploded = explode('.', $backupFile);
|
||||
array_pop($backupFileExploded);
|
||||
$tmpSql = implode('.', $backupFileExploded) . '.sql.gz';
|
||||
if (is_file($userDataPath . DIRECTORY_SEPARATOR . $tmpSql)) {
|
||||
exec('cd ' . $userDataPath . ' && rm -f ' . $tmpSql);
|
||||
}
|
||||
|
||||
return $this->deleteDir($this->tmpDir()) && $this->deleteDir($sBeforeTmp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $excludeDir
|
||||
* @param string $tmpDir extracted temporally directory
|
||||
* @param string $oldUserDataDir
|
||||
*/
|
||||
protected function excludeDirectory($excludeDir = [], $tmpDir, $oldUserDataDir)
|
||||
{
|
||||
foreach ($excludeDir as $directory) {
|
||||
// EXCLUDE WIKI DIRECTORY
|
||||
$keepPath = $tmpDir . DIRECTORY_SEPARATOR . $directory;
|
||||
$keepDirTmp = $oldUserDataDir . DIRECTORY_SEPARATOR . $directory;
|
||||
|
||||
if (!is_dir($keepPath) && $directory !== 'wiki') {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" cannot be skipped', $keepPath));
|
||||
throw new BackupException(sprintf('Directory "%s" cannot be skipped', $keepPath));
|
||||
}
|
||||
|
||||
if ($directory !== 'wiki') {
|
||||
$this->deleteDir($keepPath);
|
||||
}
|
||||
|
||||
if (file_exists($keepDirTmp)) {
|
||||
$oldDirTmp = rtrim(
|
||||
sys_get_temp_dir(),
|
||||
DIRECTORY_SEPARATOR
|
||||
) . DIRECTORY_SEPARATOR . '.' . $directory . 'Tmp' . uniqid('', true);
|
||||
if (!$this->isDir($oldDirTmp)) {
|
||||
$this->logger->writePersistent(sprintf('Directory "%s" was not created', $oldDirTmp));
|
||||
throw new BackupException(sprintf('Directory "%s" was not created', $oldDirTmp));
|
||||
}
|
||||
$oldDir = $oldDirTmp . DIRECTORY_SEPARATOR . $directory;
|
||||
if (!$this->moveDir($keepDirTmp, $oldDir)) {
|
||||
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $directory,
|
||||
$oldDir));
|
||||
throw new BackupException(sprintf('Could not move %s directory into "%s"', $directory,
|
||||
$oldDir));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset Latest WIKI Directory
|
||||
if (isset($oldDir) && is_dir($oldDir)) {
|
||||
$this->logger->write('Reset Wiki Directory');
|
||||
|
||||
if (!$this->moveDir($oldDir, $keepPath)) {
|
||||
$this->logger->writePersistent(sprintf('Could not move %s directory into "%s"', $oldDir,
|
||||
$keepPath));
|
||||
throw new BackupException(sprintf('Could not move %s directory into "%s"', $oldDir, $keepPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $userDataDir
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLockStatus($userDataDir = null)
|
||||
{
|
||||
if (null !== $userDataDir) {
|
||||
$this->sUserPath = $userDataDir;
|
||||
}
|
||||
$path = $this->tmpDir();
|
||||
if (file_exists($path . FileBackupInterface::PID_FILE) &&
|
||||
($time = file_get_contents($path . FileBackupInterface::PID_FILE)) &&
|
||||
(time() - (int)$time < FileBackupInterface::TIME_OUT)
|
||||
) {
|
||||
return FileBackupInterface::STATUS_WORKING;
|
||||
}
|
||||
|
||||
return FileBackupInterface::STATUS_WAITING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean everything without files move. This might be used, when breaking started backup job
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function breakCleanUp()
|
||||
{
|
||||
return $this->deleteDir($this->tmpDir());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Xentral\Components\Backup;
|
||||
|
||||
|
||||
use Xentral\Components\Backup\Exception\BackupException;
|
||||
|
||||
interface FileBackupInterface
|
||||
{
|
||||
/** @var string STATUS_WAIT */
|
||||
const STATUS_WAITING = 'waiting';
|
||||
/** @var string STATUS_WORKING */
|
||||
const STATUS_WORKING = 'working';
|
||||
/** @var string Extension for the whole backup */
|
||||
const COMPRESS_EXTENSION = 'zip';
|
||||
/** @var string pid file */
|
||||
const PID_FILE = 'backup.lock';
|
||||
/** @var int Timeout */
|
||||
const TIME_OUT = 3600;
|
||||
/** @var string snapshots folder */
|
||||
const SNAPSHOTS_FOLDER = 'backup/snapshots';
|
||||
/** @var string user data directory */
|
||||
const LOCAL_FILES_DIR_NAME = 'userdata';
|
||||
|
||||
/**
|
||||
* @param string|null $userPath
|
||||
*
|
||||
* @throws BackupException
|
||||
* @return string|null
|
||||
*/
|
||||
public function begin($userPath = null);
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param string $userPath
|
||||
* @param string|null $sMySQLFile
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createBackup($filename, $userPath, $sMySQLFile = null);
|
||||
|
||||
/**
|
||||
* @param string $backupFile
|
||||
* @param string $userPath
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function restoreFileSystem($backupFile, $userPath, $options = []);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLockStatus();
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Xentral\Components\Backup\Logger;
|
||||
|
||||
|
||||
use Xentral\Components\Backup\Exception\LogException;
|
||||
|
||||
final class BackupLog
|
||||
{
|
||||
/** @var string|null $path */
|
||||
private $fullPath;
|
||||
|
||||
/** @var string|null $storagePath */
|
||||
private $storagePath;
|
||||
|
||||
/** @var string|null $fileName */
|
||||
private $fileName;
|
||||
|
||||
/** @var string $persistentFile */
|
||||
private static $persistentFile = 'backup_logger.txt';
|
||||
|
||||
public function __construct($path = null, $fileName = null)
|
||||
{
|
||||
if (null !== $path && null !== $fileName) {
|
||||
$this->fullPath = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
$this->storagePath = $path;
|
||||
$this->fileName = $fileName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
*
|
||||
* @param string|null $path
|
||||
* @param string|null $fileName
|
||||
*
|
||||
* @param bool $withDate
|
||||
*
|
||||
* @param bool $append
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function write($message, $path = null, $fileName = null, $withDate = true, $append = true)
|
||||
{
|
||||
$flag = FILE_APPEND | LOCK_EX;
|
||||
|
||||
$path = $this->getFullPath($path, $fileName);
|
||||
|
||||
if (null === $path || (!file_exists($path) && !@touch($path))) {
|
||||
throw new LogException(sprintf('cannot access or create file %s', $path));
|
||||
}
|
||||
|
||||
$message = $withDate === true ? time() . ': ' . $message : $message;
|
||||
if ($append === false) {
|
||||
$flag &= ~FILE_APPEND;
|
||||
}
|
||||
|
||||
return file_put_contents($path, $message . "\n", $flag);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int $linePosition
|
||||
*
|
||||
* @param string|null $path
|
||||
*
|
||||
* @param string|null $fileName
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function tail($linePosition = 0, $path = null, $fileName = null)
|
||||
{
|
||||
$path = $this->getFullPath($path, $fileName);
|
||||
|
||||
if (null === $path || !file_exists($path)) {
|
||||
throw new LogException(sprintf('File %s cannot be found!', $path));
|
||||
}
|
||||
$output = '';
|
||||
if (($xData = file($path, FILE_SKIP_EMPTY_LINES)) && count($xData) > 0) {
|
||||
$key = (int)$linePosition === 0 ? count($xData) - 1 : $linePosition;
|
||||
if (!array_key_exists($key, $xData)) {
|
||||
throw new LogException(sprintf('Offset %d is missing', (int)$linePosition));
|
||||
}
|
||||
$output = $xData[$key];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $path
|
||||
*
|
||||
* @param string|null $fileName
|
||||
*
|
||||
* @return bool|false|string
|
||||
*/
|
||||
public function getContent($path = null, $fileName = null)
|
||||
{
|
||||
$path = $this->getFullPath($path, $fileName);
|
||||
|
||||
if (null === $path || !file_exists($path)) {
|
||||
throw new LogException(sprintf('File %s cannot be found!', $path));
|
||||
}
|
||||
|
||||
return file_get_contents($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $path
|
||||
*
|
||||
* @param string|null $fileName
|
||||
*
|
||||
* @throws LogException
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($path = null, $fileName = null)
|
||||
{
|
||||
$path = $this->getFullPath($path, $fileName);
|
||||
if (null !== $path && !file_exists($path)) {
|
||||
return false;
|
||||
//throw new LogException(sprintf('File %s cannot be deleted!', $path));
|
||||
}
|
||||
|
||||
return unlink($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $path
|
||||
* @param null $fileName
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getFullPath($path = null, $fileName = null)
|
||||
{
|
||||
if ($path === null && $fileName === null) {
|
||||
return $this->fullPath;
|
||||
}
|
||||
if ($path !== null && $fileName !== null) {
|
||||
return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
if ($fileName !== null && $path === null && $this->storagePath !== null) {
|
||||
return rtrim($this->storagePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
if ($path !== null && $fileName === null && $this->fileName !== null) {
|
||||
return rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $this->fileName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function writePersistent($message)
|
||||
{
|
||||
$this->write($message, null, self::$persistentFile, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPersistentFileName()
|
||||
{
|
||||
return self::$persistentFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode;
|
||||
|
||||
use Xentral\Components\Barcode\Exception\InvalidArgumentException;
|
||||
|
||||
final class BarcodeFactory
|
||||
{
|
||||
/**
|
||||
* @param string $codeText
|
||||
* @param string $ecLevel Error correction level [L|M|Q|H]
|
||||
*
|
||||
* @return Qrcode
|
||||
*/
|
||||
public function createQrCode($codeText, $ecLevel = 'L')
|
||||
{
|
||||
$codeType = 'QRCODE,' . $ecLevel;
|
||||
if (!in_array($codeType, Qrcode::$validTypes, true)) {
|
||||
throw new InvalidArgumentException('Invalid error correction level: ' . $ecLevel);
|
||||
}
|
||||
$barcode2d = new TcpdfBarcode2d($codeType, $codeText);
|
||||
|
||||
return new Qrcode($barcode2d);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'BarcodeFactory' => 'onInitBarcodeFactory',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BarcodeFactory
|
||||
*/
|
||||
public static function onInitBarcodeFactory()
|
||||
{
|
||||
return new BarcodeFactory();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class BarcodeCreationFailedException extends RuntimeException implements BarcodeExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface BarcodeExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements BarcodeExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class MissingPhpExtensionException extends RuntimeException implements BarcodeExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode;
|
||||
|
||||
final class Qrcode
|
||||
{
|
||||
/** @var string TYPE_QRCODE */
|
||||
const TYPE_DEFAULT = 'QRCODE';
|
||||
|
||||
/** @var string TYPE_EC_LOW Low error correction */
|
||||
const TYPE_EC_LOW = 'QRCODE,L';
|
||||
|
||||
/** @var string TYPE_EC_MEDIUM Medium error correction */
|
||||
const TYPE_EC_MEDIUM = 'QRCODE,M';
|
||||
|
||||
/** @var string TYPE_EC_QUARTILE Better error correction */
|
||||
const TYPE_EC_QUARTILE = 'QRCODE,Q';
|
||||
|
||||
/** @var string TYPE_EC_HIGH Best error correction */
|
||||
const TYPE_EC_HIGH = 'QRCODE,H';
|
||||
|
||||
/** @var array $validTypes */
|
||||
public static $validTypes = [
|
||||
self::TYPE_DEFAULT,
|
||||
self::TYPE_EC_LOW,
|
||||
self::TYPE_EC_MEDIUM,
|
||||
self::TYPE_EC_QUARTILE,
|
||||
self::TYPE_EC_HIGH,
|
||||
];
|
||||
|
||||
/** @var TcpdfBarcode2d $barcode */
|
||||
private $barcode;
|
||||
|
||||
/**
|
||||
* @param TcpdfBarcode2d $barcode
|
||||
*/
|
||||
public function __construct(TcpdfBarcode2d $barcode)
|
||||
{
|
||||
$this->barcode = $barcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->barcode->getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as array representation
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->barcode->getBarcodeArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as HTML representation
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in pixels.
|
||||
* @param int $height Height of a single rectangle element in pixels.
|
||||
* @param string $color Foreground color for bar elements (background is transparent).
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function toHtml($width = 10, $height = 10, $color = 'black')
|
||||
{
|
||||
return $this->barcode->getBarcodeHtml($width, $height, $color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as SVG document
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in user units
|
||||
* @param int $height Height of a single rectangle element in user units
|
||||
* @param string $color Foreground color (in SVG format) for bar elements (background is transparent)
|
||||
*
|
||||
* @return string SVG document
|
||||
*/
|
||||
public function toSvg($width = 10, $height = 10, $color = 'black')
|
||||
{
|
||||
return $this->barcode->getBarcodeSvg($width, $height, $color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as PNG image (requires GD or Imagick library)
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in pixels
|
||||
* @param int $height Height of a single rectangle element in pixels
|
||||
* @param array $color RGB-Array (0-255) foreground color for bar elements (background is transparent)
|
||||
*
|
||||
* @return string Image as string
|
||||
*/
|
||||
public function toPng($width = 10, $height = 10, $color = [0, 0, 0])
|
||||
{
|
||||
return $this->barcode->getBarcodePng($width, $height, $color);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Barcode;
|
||||
|
||||
use TCPDF2DBarcode;
|
||||
use Xentral\Components\Barcode\Exception\BarcodeCreationFailedException;
|
||||
use Xentral\Components\Barcode\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Barcode\Exception\MissingPhpExtensionException;
|
||||
|
||||
/**
|
||||
* Anti-Corruption-Layer for TCPDF2DBarcode class
|
||||
*/
|
||||
final class TcpdfBarcode2d
|
||||
{
|
||||
/** @var array $validTypes */
|
||||
public static $validTypes = [
|
||||
Qrcode::TYPE_DEFAULT,
|
||||
Qrcode::TYPE_EC_LOW,
|
||||
Qrcode::TYPE_EC_MEDIUM,
|
||||
Qrcode::TYPE_EC_QUARTILE,
|
||||
Qrcode::TYPE_EC_HIGH,
|
||||
];
|
||||
|
||||
/** @var TCPDF2DBarcode $barcode */
|
||||
private $barcode;
|
||||
|
||||
/** @var string $codeType */
|
||||
private $type;
|
||||
|
||||
/** @var string $codeText */
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $text
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws BarcodeCreationFailedException
|
||||
*/
|
||||
public function __construct($type, $text)
|
||||
{
|
||||
if (empty($type)) {
|
||||
throw new InvalidArgumentException('Could not create barcode. Required parameter "type" is empty.');
|
||||
}
|
||||
if (empty($text)) {
|
||||
throw new InvalidArgumentException('Could not create barcode. Required parameter "text" is empty.');
|
||||
}
|
||||
if (!in_array($type, self::$validTypes, true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Could not create barcode. Invalid Type: "%s". Valid types: [%s]',
|
||||
$type,
|
||||
implode('|', self::$validTypes)
|
||||
));
|
||||
}
|
||||
|
||||
$barcode = new TCPDF2DBarcode($text, $type);
|
||||
if ($barcode->getBarcodeArray() === false) {
|
||||
throw new BarcodeCreationFailedException(sprintf(
|
||||
'Could not create barcode. Type "%s" - Text "%s"', $type, $text
|
||||
));
|
||||
}
|
||||
|
||||
$this->barcode = $barcode;
|
||||
$this->text = $text;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getBarcodeArray()
|
||||
{
|
||||
return $this->barcode->getBarcodeArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as HTML representation
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in pixels.
|
||||
* @param int $height Height of a single rectangle element in pixels.
|
||||
* @param string $color Foreground color for bar elements (background is transparent).
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function getBarcodeHtml($width = 10, $height = 10, $color = 'black')
|
||||
{
|
||||
return $this->barcode->getBarcodeHTML($width, $height, $color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as SVG document
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in user units
|
||||
* @param int $height Height of a single rectangle element in user units
|
||||
* @param string $color Foreground color (in SVG format) for bar elements (background is transparent)
|
||||
*
|
||||
* @return string SVG document
|
||||
*/
|
||||
public function getBarcodeSvg($width = 10, $height = 10, $color = 'black')
|
||||
{
|
||||
return $this->barcode->getBarcodeSVGcode($width, $height, $color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the QR code as PNG image (requires GD or Imagick library)
|
||||
*
|
||||
* @param int $width Width of a single rectangle element in pixels
|
||||
* @param int $height Height of a single rectangle element in pixels
|
||||
* @param array $color RGB-Array (0-255) foreground color for bar elements (background is transparent)
|
||||
*
|
||||
* @throws MissingPhpExtensionException If gd and imagick extension missing
|
||||
* @throws BarcodeCreationFailedException
|
||||
*
|
||||
* @return string Image as string
|
||||
*/
|
||||
public function getBarcodePng($width, $height, $color = [0, 0, 0])
|
||||
{
|
||||
$imageData = $this->barcode->getBarcodePngData($width, $height, $color);
|
||||
if (!function_exists('imagecreate') && !extension_loaded('imagick')) {
|
||||
throw new MissingPhpExtensionException(
|
||||
'Barcode image creation failed. PHP extension "gd" or "imagick" is required; both missing.'
|
||||
);
|
||||
}
|
||||
if ($imageData === false) {
|
||||
throw new BarcodeCreationFailedException(
|
||||
'Barcode image creation failed. Unknown error.'
|
||||
);
|
||||
}
|
||||
|
||||
return $imageData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Barcodes
|
||||
|
||||
## QR-Codes
|
||||
|
||||
### Beispiel
|
||||
|
||||
```php
|
||||
$factory = $this->app->Container->get('BarcodeFactory');
|
||||
|
||||
// Qrcode-Objekt erzeugen
|
||||
$ecLevel = 'M'; // M = Medium error correction
|
||||
$qrcode = $factory->createQrCode($qrtext, $ecLevel);
|
||||
|
||||
// Varianten für die Ausgabe
|
||||
$html = $qrcode->toHtml($width, $height, $color);
|
||||
$svg = $qrcode->toSvg($width, $height, $color);
|
||||
$png = $qrcode->toPng($width, $height, $color);
|
||||
```
|
||||
|
||||
##### Fehlerkorrektur-Levels
|
||||
|
||||
* `L` = Low / Niedrige Fehlerkorrektur (Default)
|
||||
* `M` = Medium / Mittlere Fehlerkorrektur
|
||||
* `Q` = Quartile / Bessere Fehlerkorrektur
|
||||
* `H` = High / Höchste Fehlerkorrektur
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Adapter;
|
||||
|
||||
use Generator;
|
||||
|
||||
interface AdapterInterface
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function connect();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rollback();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function commit();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId();
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param string $statement
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchRow($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchCol($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value);
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Adapter;
|
||||
|
||||
use Generator;
|
||||
use mysqli;
|
||||
use mysqli_result;
|
||||
use mysqli_stmt;
|
||||
use Xentral\Components\Database\DatabaseConfig;
|
||||
use Xentral\Components\Database\Exception\BindParameterException;
|
||||
use Xentral\Components\Database\Exception\ConnectionException;
|
||||
use Xentral\Components\Database\Exception\EscapingException;
|
||||
use Xentral\Components\Database\Exception\QueryFailureException;
|
||||
use Xentral\Components\Database\Exception\TransactionException;
|
||||
use Xentral\Components\Database\Parser\MysqliArrayValueParser;
|
||||
use Xentral\Components\Database\Parser\MysqliNamedParameterParser;
|
||||
use Xentral\Components\Database\Profiler\ProfilerInterface;
|
||||
|
||||
final class MysqliAdapter implements AdapterInterface
|
||||
{
|
||||
/** @var mysqli|null $connection */
|
||||
private $connection;
|
||||
|
||||
/** @var DatabaseConfig $config */
|
||||
private $config;
|
||||
|
||||
/** @var bool $transactionActive */
|
||||
private $transactionActive = false;
|
||||
|
||||
/** @var int|null $reconnectCounter */
|
||||
private $reconnectCounter;
|
||||
|
||||
/** @var int $reconnectLimit */
|
||||
private $reconnectMaxCount = 5;
|
||||
|
||||
/** @var ProfilerInterface|null $profiler */
|
||||
private $profiler;
|
||||
|
||||
/**
|
||||
* @param DatabaseConfig $config
|
||||
* @param ProfilerInterface|null $profiler
|
||||
*/
|
||||
public function __construct(DatabaseConfig $config, ProfilerInterface $profiler = null)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->profiler = $profiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($this->connection !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->reconnectCounter === null) {
|
||||
$this->reconnectCounter = 0;
|
||||
} else {
|
||||
$this->reconnectCounter++;
|
||||
}
|
||||
|
||||
if ($this->reconnectCounter >= $this->reconnectMaxCount) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Too many reconnects. Reconnect count: %d (Max allowed %d)',
|
||||
$this->reconnectCounter,
|
||||
$this->reconnectMaxCount
|
||||
));
|
||||
}
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
|
||||
$connection = new mysqli(
|
||||
$this->config->getHostname(),
|
||||
$this->config->getUsername(),
|
||||
$this->config->getPassword(),
|
||||
null,
|
||||
$this->config->getPort()
|
||||
);
|
||||
|
||||
if ($connection->connect_errno > 0) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Database connection to host "%s" failed. Error code #%s. Error message: %s',
|
||||
$this->config->getHostname(),
|
||||
$connection->connect_errno,
|
||||
$connection->connect_error
|
||||
));
|
||||
}
|
||||
|
||||
$connection->select_db($this->config->getDatabase());
|
||||
if ($connection->errno > 0) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Database selection failed for database "%s". Error code #%s. Error message: %s',
|
||||
$this->config->getDatabase(),
|
||||
$connection->errno,
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
// @see https://www.php.net/manual/de/mysqlinfo.concepts.charset.php
|
||||
if (!$connection->set_charset($this->config->getCharset())) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Failed to set character set "%s". Error: %s',
|
||||
$this->config->getCharset(),
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
if (!$connection->autocommit(true)) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Failed to activate auto commit. Error: %s',
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
$this->finishProfiler(null, [
|
||||
'dbname' => $this->config->getDatabase(),
|
||||
'host' => $this->config->getHostname(),
|
||||
'port' => $this->config->getPort(),
|
||||
]);
|
||||
|
||||
$this->connection = $connection;
|
||||
|
||||
foreach ($this->config->getQueries() as $query) {
|
||||
$this->perform($query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->connection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$this->connection->close();
|
||||
$this->connection = null;
|
||||
$this->finishProfiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction()
|
||||
{
|
||||
return $this->transactionActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException If transaction is already started
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
if ($this->inTransaction()) {
|
||||
throw new TransactionException('Transaction is already started.');
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
|
||||
if ($this->connection->begin_transaction() === false) {
|
||||
throw new TransactionException(sprintf('Transaction start failed: %s', $this->connection->error));
|
||||
}
|
||||
$this->transactionActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
if (!$this->inTransaction()) {
|
||||
throw new TransactionException('Transaction not started.');
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
$this->connection->autocommit(true);
|
||||
$this->transactionActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rollback()
|
||||
{
|
||||
if (!$this->inTransaction()) {
|
||||
throw new TransactionException('Transaction not started.');
|
||||
}
|
||||
|
||||
$this->connection->rollback();
|
||||
$this->connection->autocommit(true);
|
||||
$this->transactionActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return (int)$this->connection->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param string $statement
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
if (!is_object($query)) {
|
||||
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
|
||||
}
|
||||
|
||||
$query->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
if (!is_object($query)) {
|
||||
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
|
||||
}
|
||||
|
||||
$affectedRows = (int)$query->affected_rows;
|
||||
$query->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $affectedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$assocKey = reset($row); // Fetch first array value
|
||||
$data[$assocKey] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchRow($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->num_rows === 0) {
|
||||
$result->close();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $result->fetch_assoc();
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->num_rows === 0) {
|
||||
$result->close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $result->fetch_assoc();
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return reset($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchCol($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$firstValue = reset($row); // Fetch first array value
|
||||
$data[] = $firstValue;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->field_count !== 2) {
|
||||
throw new QueryFailureException('Field count does not match. fetchPairs() allows only two fields.');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$key = array_shift($row);
|
||||
$value = array_shift($row);
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false)
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
$includeGroupColumn = (bool)$includeGroupColumn;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$group = $includeGroupColumn === true ? reset($row) : array_shift($row); // Fetch first array value
|
||||
if (!isset($data[$group])) {
|
||||
$data[$group] = [];
|
||||
}
|
||||
$data[$group][] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$assocKey = reset($row); // Fetch first array value
|
||||
|
||||
yield $assocKey => $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$firstValue = reset($row); // Fetch first array value
|
||||
|
||||
yield $firstValue;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
if ($result->field_count !== 2) {
|
||||
throw new QueryFailureException('Field count does not match. yieldPairs() allows only two fields.');
|
||||
}
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$key = array_shift($row);
|
||||
$value = array_shift($row);
|
||||
|
||||
yield $key => $value;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_bool($value)) {
|
||||
throw new EscapingException('Can not escape bool. Value is not a bool.');
|
||||
}
|
||||
|
||||
return $value === true ? '1' : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_int($value)) {
|
||||
throw new EscapingException('Can not escape integer. Value is not an integer.');
|
||||
}
|
||||
|
||||
return (string)(int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
throw new EscapingException('Can not escape decimal. Value is not numeric.');
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
throw new EscapingException('Can not escape string. Value is not a string.');
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
|
||||
return "'" . $this->connection->real_escape_string($value) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new EscapingException('Can not escape identifier. Passed value is not a string.');
|
||||
}
|
||||
if (empty(trim($value))) {
|
||||
throw new EscapingException('Can not escape identifier. Passed value is empty.');
|
||||
}
|
||||
|
||||
$parts = explode('.', $value);
|
||||
if (count($parts) > 2) {
|
||||
throw new EscapingException('Can not escape identifier. Identifier contains more than one dots.');
|
||||
}
|
||||
|
||||
$partsCleaned = [];
|
||||
foreach ($parts as $part) {
|
||||
if (empty(trim($part))) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Parts before and after the dot can not be empty.'
|
||||
);
|
||||
}
|
||||
if (strlen($part) > 64) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Identifier is too long. Only 64 characters are allowed.'
|
||||
);
|
||||
}
|
||||
$partCleaned = preg_replace('/[^A-Za-z0-9_]+/', '', $part);
|
||||
if (strlen($partCleaned) !== strlen($part)) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Passed value contains invalid characters. ' .
|
||||
'Valid characters: A-Z, a-z, 0-9, Underscore'
|
||||
);
|
||||
}
|
||||
|
||||
$partsCleaned[] = '`' . $partCleaned . '`';
|
||||
}
|
||||
|
||||
return implode('.', $partsCleaned);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->connection = null;
|
||||
$this->transactionActive = false;
|
||||
$this->config = clone $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return mysqli_stmt
|
||||
*/
|
||||
private function getMysqliStatement($statement, array $values = [])
|
||||
{
|
||||
list($statement, $values) = $this->replaceArrayValues($statement, $values);
|
||||
list($rebuildStatement, $bindValues, $parameterNames) = $this->replaceNamedParameters($statement, $values);
|
||||
|
||||
$query = $this->connection->prepare($rebuildStatement);
|
||||
if ($query === false && $this->connection->errno === 2006) {
|
||||
// Code 2006 = MySQL server has gone away
|
||||
// Falls Verbindung in einen Timeout gelaufen ist
|
||||
// => Verbindung wiederherstellen und Prepare erneut probieren
|
||||
$this->disconnect();
|
||||
$this->connect();
|
||||
$query = $this->connection->prepare($rebuildStatement);
|
||||
}
|
||||
if ($query === false || !is_object($query)) {
|
||||
throw new QueryFailureException(
|
||||
sprintf(
|
||||
'Database prepare failed. Error code #%s. Error message: %s',
|
||||
$this->connection->errno,
|
||||
$this->connection->error
|
||||
),
|
||||
(int)$this->connection->errno
|
||||
);
|
||||
}
|
||||
|
||||
$this->bindParametersToMysqliStatement($query, $bindValues, $parameterNames);
|
||||
$query->execute();
|
||||
|
||||
if ($query->errno > 0) {
|
||||
throw new QueryFailureException(
|
||||
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
|
||||
(int)$query->errno
|
||||
);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceArrayValues($statement, array $values = [])
|
||||
{
|
||||
$parser = new MysqliArrayValueParser();
|
||||
$result = $parser->rebuild($statement, $values);
|
||||
|
||||
return [$result['statement'], $result['values']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceNamedParameters($statement, array $values = [])
|
||||
{
|
||||
$parser = new MysqliNamedParameterParser();
|
||||
$result = $parser->rebuild($statement, $values);
|
||||
|
||||
return [$result['statement'], $result['values'], $result['params']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli_stmt $statement
|
||||
* @param array $bindValues Values for binding
|
||||
* @param array $parameterNames Original parameter names (for debugging only)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function bindParametersToMysqliStatement($statement, $bindValues, $parameterNames)
|
||||
{
|
||||
if (empty($bindValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bindTypes = '';
|
||||
foreach ($bindValues as $index => &$bindValue) {
|
||||
if (is_bool($bindValue)) {
|
||||
$bindValue = (int)$bindValue;
|
||||
$bindTypes .= 'i'; // integer
|
||||
continue;
|
||||
}
|
||||
if (is_float($bindValue)) {
|
||||
$bindTypes .= 'd'; // double
|
||||
continue;
|
||||
}
|
||||
if (is_array($bindValue)) {
|
||||
throw new BindParameterException(sprintf(
|
||||
'Can not bind parameter of type "array" to placeholder "%s".',
|
||||
$parameterNames[$index]
|
||||
));
|
||||
}
|
||||
if (is_object($bindValue)) {
|
||||
throw new BindParameterException(sprintf(
|
||||
'Can not bind parameter of type "object" to placeholder "%s".',
|
||||
$parameterNames[$index]
|
||||
));
|
||||
}
|
||||
$bindTypes .= 's'; // string
|
||||
}
|
||||
unset($bindValue);
|
||||
|
||||
$statement->bind_param($bindTypes, ...$bindValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return mysqli_result
|
||||
*/
|
||||
private function getMysqliResult($statement, array $values = [])
|
||||
{
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
$result = $query->get_result();
|
||||
$query->close();
|
||||
|
||||
if ($result === false) {
|
||||
throw new QueryFailureException(
|
||||
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
|
||||
(int)$query->errno
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function startProfiler($methodName)
|
||||
{
|
||||
if ($this->profiler === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->profiler->start(__CLASS__, $methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function finishProfiler($statement = null, array $values = [])
|
||||
{
|
||||
if ($this->profiler === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->profiler->finish($statement, $values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
use Xentral\Components\Database\Adapter\MysqliAdapter;
|
||||
use Xentral\Components\Database\Exception\ConfigException;
|
||||
use Xentral\Components\Database\Profiler\Profiler;
|
||||
use Xentral\Components\Database\SqlQuery\QueryFactory;
|
||||
use Xentral\Components\Logger\Context\ContextHelper;
|
||||
use Xentral\Components\Logger\MemoryLogger;
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Xentral\Core\DependencyInjection\ServiceContainer;
|
||||
use Xentral\Core\LegacyConfig\ConfigLoader;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'Database' => 'onInitDatabase',
|
||||
'DatabaseProfiler' => 'onGetDatabaseProfiler',
|
||||
'MysqliAdapter' => 'onInitMysqliAdapter',
|
||||
'QueryFactory' => 'onInitQueryFactory',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return Database
|
||||
*/
|
||||
public static function onInitDatabase(ServiceContainer $container)
|
||||
{
|
||||
return new Database($container->get('MysqliAdapter'), $container->get('QueryFactory'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryFactory
|
||||
*/
|
||||
public static function onInitQueryFactory()
|
||||
{
|
||||
return new QueryFactory('mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return Profiler
|
||||
*/
|
||||
public static function onGetDatabaseProfiler(ServiceContainer $container)
|
||||
{
|
||||
$request = $container->get('Request');
|
||||
return new Profiler(new MemoryLogger(new ContextHelper($request)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return MysqliAdapter
|
||||
*/
|
||||
public static function onInitMysqliAdapter(ContainerInterface $container)
|
||||
{
|
||||
$conf = ConfigLoader::load();
|
||||
|
||||
$dbHost = property_exists($conf, 'WFdbhost') ? $conf->WFdbhost : 'localhost';
|
||||
$dbPort = property_exists($conf, 'WFdbport') ? $conf->WFdbport : 3306;
|
||||
$dbName = property_exists($conf, 'WFdbname') ? $conf->WFdbname : null;
|
||||
$dbUser = property_exists($conf, 'WFdbuser') ? $conf->WFdbuser : null;
|
||||
$dbPass = property_exists($conf, 'WFdbpass') ? $conf->WFdbpass : null;
|
||||
|
||||
if (empty($dbName)) {
|
||||
throw new ConfigException('Could not connect to database. Database name is missing or empty.');
|
||||
}
|
||||
if (empty($dbUser)) {
|
||||
throw new ConfigException('Could not connect to database. Database user is missing or empty.');
|
||||
}
|
||||
if (empty($dbPass)) {
|
||||
throw new ConfigException('Could not connect to database. Database password is missing or empty.');
|
||||
}
|
||||
|
||||
$startupQueries = [
|
||||
"SET NAMES 'utf8', " .
|
||||
"CHARACTER SET 'utf8', " .
|
||||
"lc_time_names = 'de_DE', " .
|
||||
"SESSION sql_mode = '', " .
|
||||
"SESSION sql_big_selects = 1;",
|
||||
];
|
||||
$config = new DatabaseConfig($dbHost, $dbUser, $dbPass, $dbName, 'utf8', $dbPort, $startupQueries);
|
||||
|
||||
// Profiler aktivieren
|
||||
// Kann mit $container->get('DatabaseProfiler')->getContexts() abgefragt werden
|
||||
$profiler = $container->get('DatabaseProfiler');
|
||||
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE === true) {
|
||||
$profiler->setActive(true);
|
||||
}
|
||||
|
||||
return new MysqliAdapter($config, $profiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php /** @noinspection PhpInconsistentReturnPointsInspection */
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
use Generator;
|
||||
use Xentral\Components\Database\Adapter\AdapterInterface;
|
||||
use Xentral\Components\Database\Exception\EscapingException;
|
||||
use Xentral\Components\Database\Exception\TransactionException;
|
||||
use Xentral\Components\Database\SqlQuery\DeleteQuery;
|
||||
use Xentral\Components\Database\SqlQuery\InsertQuery;
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Components\Database\SqlQuery\QueryFactory;
|
||||
use Xentral\Components\Database\SqlQuery\UpdateQuery;
|
||||
|
||||
final class Database
|
||||
{
|
||||
/** @var AdapterInterface $adapter */
|
||||
private $adapter;
|
||||
|
||||
/** @var QueryFactory $queryFactory */
|
||||
private $queryFactory;
|
||||
|
||||
/**
|
||||
* @param AdapterInterface $adapter
|
||||
* @param QueryFactory $queryFactory
|
||||
*/
|
||||
public function __construct(AdapterInterface $adapter, QueryFactory $queryFactory)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
$this->queryFactory = $queryFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aura.SqlQuery
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function select()
|
||||
{
|
||||
return $this->queryFactory->newSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InsertQuery
|
||||
*/
|
||||
public function insert()
|
||||
{
|
||||
return $this->queryFactory->newInsert();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UpdateQuery
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
return $this->queryFactory->newUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteQuery
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
return $this->queryFactory->newDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* ENDE: Aura.SqlQuery
|
||||
*/
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->adapter->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes simple queries without named parameters
|
||||
*
|
||||
* Use self::perform() for queries with named parameters.
|
||||
*
|
||||
* @param string $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function exec($query)
|
||||
{
|
||||
$this->adapter->perform($query, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return (int)$this->adapter->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException If transaction is already started
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->adapter->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
$this->adapter->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rollBack()
|
||||
{
|
||||
$this->adapter->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction()
|
||||
{
|
||||
return $this->adapter->inTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchAll($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAll($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchAssoc($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAssoc($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchGroup($query, array $values = [], $includeGroupColumn = false)
|
||||
{
|
||||
return $this->adapter->fetchGroup($query, $values, (bool)$includeGroupColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchRow($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchRow($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchPairs($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchPairs($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchValue($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAffected($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchCol($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchCol($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldAll($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldAssoc($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldPairs($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldCol($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($query, array $values = [])
|
||||
{
|
||||
$this->adapter->perform($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeBool($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeInt($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeDecimal($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeString($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value)
|
||||
{
|
||||
return $this->adapter->escapeIdentifier($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
final class DatabaseConfig
|
||||
{
|
||||
/** @var string $hostname */
|
||||
private $hostname;
|
||||
|
||||
/** @var string $username */
|
||||
private $username;
|
||||
|
||||
/** @var string $password */
|
||||
private $password;
|
||||
|
||||
/** @var string $database */
|
||||
private $database;
|
||||
|
||||
/** @var string $charset */
|
||||
private $charset;
|
||||
|
||||
/** @var int $port */
|
||||
private $port;
|
||||
|
||||
/** @var array $queries */
|
||||
private $queries;
|
||||
|
||||
/**
|
||||
* @param string $hostname
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $database
|
||||
* @param string|null $charset
|
||||
* @param int|null $port
|
||||
* @param array $queries
|
||||
*/
|
||||
public function __construct(
|
||||
$hostname,
|
||||
$username,
|
||||
$password,
|
||||
$database,
|
||||
$charset = null,
|
||||
$port = null,
|
||||
array $queries = []
|
||||
) {
|
||||
$this->hostname = (string)$hostname;
|
||||
$this->username = (string)$username;
|
||||
$this->password = (string)$password;
|
||||
$this->database = (string)$database;
|
||||
$this->charset = $charset !== null ? (string)$charset : 'utf8';
|
||||
$this->port = $port !== null ? (int)$port : 3306;
|
||||
$this->queries = $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
*
|
||||
* @return DatabaseConfig
|
||||
*/
|
||||
public static function fromArray(array $config)
|
||||
{
|
||||
return new DatabaseConfig(
|
||||
$config['hostname'],
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
$config['database'],
|
||||
isset($config['charset']) ? $config['charset'] : null,
|
||||
isset($config['port']) ? $config['port'] : null,
|
||||
isset($config['queries']) ? $config['queries'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabase()
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQueries()
|
||||
{
|
||||
return $this->queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'args' => [
|
||||
'hostname' => $this->hostname,
|
||||
'username' => '****',
|
||||
'password' => '****',
|
||||
'database' => $this->database,
|
||||
'charset' => $this->charset,
|
||||
'port' => $this->port,
|
||||
'queries' => $this->queries,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class BindParameterException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class ConfigException extends \InvalidArgumentException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class ConnectionException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in 19.4
|
||||
*/
|
||||
class DatabaseException extends RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface DatabaseExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class EscapingException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class MissingParameterException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class NotConnectedException extends \LogicException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class QueryFailureException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class TransactionException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
/**
|
||||
* @example
|
||||
* self::rebuild('SELECT * FROM foo WHERE id IN (:ids)', ['ids' => [1, 2, 3]])
|
||||
* Erzeugt:
|
||||
* [
|
||||
* 'statement' => 'SELECT * FROM foo WHERE id IN (:ids_expl_0_, :ids_expl_1_, :ids_expl_2_)',
|
||||
* 'values' => [
|
||||
* '_ids_expl_0_' => 1,
|
||||
* '_ids_expl_1_' => 2,
|
||||
* '_ids_expl_2_' => 3,
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
final class MysqliArrayValueParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
* - Array key 'statement' contains the rebuild statement
|
||||
* - Array key 'values' contains the rebuild bind parameters
|
||||
*/
|
||||
public function rebuild($statement, array $values = [])
|
||||
{
|
||||
return $this->replaceArrayValues($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceArrayValues($statement, array $values = [])
|
||||
{
|
||||
foreach ($values as $paramName => $paramValue) {
|
||||
if (is_array($paramValue)) {
|
||||
$counter = 0;
|
||||
$additionalParams = [];
|
||||
foreach ($paramValue as $arrayValue) {
|
||||
$additionalParamName = '_' . $paramName . '_expl_' . $counter . '_';
|
||||
$additionalParams[] = ':' . $additionalParamName;
|
||||
$values[$additionalParamName] = $arrayValue;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// Replace original named parameter by exploded parameters in statement
|
||||
$replaceString = implode(', ', $additionalParams);
|
||||
$statement = str_replace(':' . $paramName, $replaceString, $statement);
|
||||
|
||||
// Remove original parameter value
|
||||
unset($values[$paramName]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'statement' => $statement,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
use Xentral\Components\Database\Exception\MissingParameterException;
|
||||
|
||||
/**
|
||||
* Responsibility of this class is to make sql statement and bind values compatible with mysqli
|
||||
*
|
||||
* (Mysqli does not support named parameters)
|
||||
*
|
||||
* It does this by:
|
||||
* - Replacing named parameters (:param) by ?-Placeholder (in statement)
|
||||
* - Rearranging bind values in order of appearance of named parameters
|
||||
*/
|
||||
final class MysqliNamedParameterParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rebuild($statement, array $values = [])
|
||||
{
|
||||
return $this->replaceNamedParameters($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws MissingParameterException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceNamedParameters($statement, array $values = [])
|
||||
{
|
||||
$result = [
|
||||
'statement' => $statement,
|
||||
'values' => [],
|
||||
'params' => [],
|
||||
];
|
||||
|
||||
if (empty($values)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Split statement on named parameters
|
||||
$parts = preg_split('/(:[a-zA-Z0-9_]+)/um', $statement, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach ($parts as &$part) {
|
||||
if (strpos($part, ':') !== 0) {
|
||||
continue; // SQL part does not contain named parameter
|
||||
}
|
||||
|
||||
$parameterName = substr_replace($part, '', 0, 1);
|
||||
if (!array_key_exists($parameterName, $values)) {
|
||||
throw new MissingParameterException(sprintf(
|
||||
'Parameter "%s" is missing from the bound values',
|
||||
$parameterName
|
||||
));
|
||||
}
|
||||
|
||||
// Push values in same order of parameters for binding
|
||||
$result['values'][] = $values[$parameterName];
|
||||
$result['params'][] = $parameterName; // For debugging only
|
||||
|
||||
// Replace named parameter by ?-Placeholder
|
||||
$part = '?';
|
||||
}
|
||||
unset($part);
|
||||
|
||||
// Rebuild statement from (changed) parts
|
||||
$result['statement'] = implode('', $parts);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
* - Array key 'statement' contains the corrected statement
|
||||
* - Array key 'values' contains the corrected bind values
|
||||
*/
|
||||
public function rebuild($statement, array $values = []);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Profiler;
|
||||
|
||||
use Exception;
|
||||
use Xentral\Components\Logger\LoggerInterface;
|
||||
|
||||
final class Profiler implements ProfilerInterface
|
||||
{
|
||||
/** @var LoggerInterface $logger */
|
||||
private $logger;
|
||||
|
||||
/** @var bool $active */
|
||||
private $active = false;
|
||||
|
||||
/** @var string $logLevel */
|
||||
private $logLevel = 'debug';
|
||||
|
||||
/** @var string $logFormat */
|
||||
private $logFormat = "{method} ({duration}): {statement} \n{backtrace}";
|
||||
|
||||
/** @var array $context */
|
||||
private $context = [];
|
||||
|
||||
/** @var array $contexts */
|
||||
private $contexts = [];
|
||||
|
||||
/**
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function __construct(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start($className, $methodName)
|
||||
{
|
||||
if (!$this->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->context = [
|
||||
'class' => $className,
|
||||
'method' => $methodName,
|
||||
'start' => microtime(true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish($statement = null, array $values = [])
|
||||
{
|
||||
if (!$this->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$finish = microtime(true);
|
||||
$exception = new Exception();
|
||||
|
||||
$this->context['finish'] = $finish;
|
||||
$this->context['duration_real'] = $finish - $this->context['start'];
|
||||
$this->context['duration'] = sprintf('%.6f', $this->context['duration_real']) . ' seconds';
|
||||
$this->context['statement'] = $statement;
|
||||
$this->context['bindings'] = $values;
|
||||
$this->context['backtrace'] = $exception->getTraceAsString();
|
||||
|
||||
$this->logger->log($this->logLevel, $this->logFormat, $this->context);
|
||||
|
||||
$this->contexts[] = $this->context;
|
||||
$this->context = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setActive($active)
|
||||
{
|
||||
$this->active = (bool)$active;
|
||||
}
|
||||
|
||||
/**
|
||||
* /**
|
||||
* @return string
|
||||
*/
|
||||
public function getLogLevel()
|
||||
{
|
||||
return $this->logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $logLevel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogLevel($logLevel)
|
||||
{
|
||||
$this->logLevel = (string)$logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
public function getLogger()
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getContexts()
|
||||
{
|
||||
return $this->contexts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Profiler;
|
||||
|
||||
use Xentral\Components\Logger\LoggerInterface;
|
||||
|
||||
interface ProfilerInterface
|
||||
{
|
||||
/**
|
||||
* @param string $className
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start($className, $methodName);
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish($statement = null, array $values = []);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive();
|
||||
|
||||
/**
|
||||
* @param bool $active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setActive($active);
|
||||
|
||||
/**
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLogLevel();
|
||||
|
||||
/**
|
||||
* @param string $logLevel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogLevel($logLevel);
|
||||
|
||||
/**
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
public function getLogger();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Delete;
|
||||
|
||||
final class DeleteQuery extends Delete
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Insert;
|
||||
|
||||
final class InsertQuery extends Insert
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\AbstractQuery;
|
||||
use Aura\SqlQuery\QueryFactory as AuraQueryFactory;
|
||||
|
||||
final class QueryFactory extends AuraQueryFactory
|
||||
{
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function newSelect()
|
||||
{
|
||||
return $this->newInstance('Select');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InsertQuery
|
||||
*/
|
||||
public function newInsert()
|
||||
{
|
||||
$insert = $this->newInstance('Insert');
|
||||
$insert->setLastInsertIdNames($this->last_insert_id_names);
|
||||
|
||||
return $insert;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UpdateQuery
|
||||
*/
|
||||
public function newUpdate()
|
||||
{
|
||||
return $this->newInstance('Update');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteQuery
|
||||
*/
|
||||
public function newDelete()
|
||||
{
|
||||
return $this->newInstance('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query The query object type.
|
||||
*
|
||||
* @return AbstractQuery
|
||||
*/
|
||||
protected function newInstance($query)
|
||||
{
|
||||
$class = "Xentral\\Components\\Database\\SqlQuery\\{$query}Query";
|
||||
|
||||
return new $class(
|
||||
$this->getQuoter(),
|
||||
$this->newSeqBindPrefix()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Select;
|
||||
use Closure;
|
||||
|
||||
final class SelectQuery extends Select
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasOrderBy()
|
||||
{
|
||||
return !empty($this->order_by);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $andor
|
||||
* @param array|callable $args
|
||||
*
|
||||
* @return Select
|
||||
*/
|
||||
protected function addWhere($andor, $args)
|
||||
{
|
||||
if ($args[0] instanceof Closure) {
|
||||
$this->addClauseCondClosure('where', $andor, $args[0]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return parent::addWhere($andor, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aura.SqlQuery 2.x unterstützt keine Klammersetzung in WHERE-Bedingungen
|
||||
*
|
||||
* @see https://github.com/auraphp/Aura.SqlQuery/issues/97
|
||||
*
|
||||
* Feature aus Version 3 importiert: https://github.com/auraphp/Aura.SqlQuery/pull/136/files
|
||||
*
|
||||
* @copyright Paul M. Jones
|
||||
* @license MIT
|
||||
*
|
||||
* @param string $clause
|
||||
* @param string $andor
|
||||
* @param callable $closure
|
||||
*/
|
||||
protected function addClauseCondClosure($clause, $andor, $closure)
|
||||
{
|
||||
// retain the prior set of conditions, and temporarily reset the clause
|
||||
// for the closure to work with (otherwise there will be an extraneous
|
||||
// opening AND/OR keyword)
|
||||
$set = $this->$clause;
|
||||
$this->$clause = [];
|
||||
|
||||
// invoke the closure, which will re-populate the $this->$clause
|
||||
$closure($this);
|
||||
|
||||
// are there new clause elements?
|
||||
if (!$this->$clause) {
|
||||
// no: restore the old ones, and done
|
||||
$this->$clause = $set;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// append an opening parenthesis to the prior set of conditions,
|
||||
// with AND/OR as needed ...
|
||||
if ($set) {
|
||||
$set[] = "{$andor} (";
|
||||
} else {
|
||||
$set[] = "(";
|
||||
}
|
||||
|
||||
// append the new conditions to the set, with indenting
|
||||
foreach ($this->$clause as $cond) {
|
||||
$set[] = " {$cond}";
|
||||
}
|
||||
$set[] = ")";
|
||||
|
||||
// ... then put the full set of conditions back into $this->$clause
|
||||
$this->$clause = $set;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Update;
|
||||
|
||||
final class UpdateQuery extends Update
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# Data Manipulation
|
||||
|
||||
## `perform()`
|
||||
|
||||
Für alle SQL Anweisung außer `SELECT`. Die Methode gibt nichts zurück. Sollte die Ausführung
|
||||
fehlschlagen, so wird eine Exception geworfen.
|
||||
|
||||
```php
|
||||
$sql = 'UPDATE foo SET bar = :bar WHERE id = :id';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
'id' => 123,
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
```
|
||||
|
||||
## `fetchAffected()`
|
||||
|
||||
Für `INSERT`, `REPLACE`, `UPDATE` und `DELETE` Anweisungen. Gibt die Anzahl der betroffenen Datensätze zurück.
|
||||
|
||||
```php
|
||||
$sql = 'UPDATE foo SET bar = :bar WHERE 1';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
];
|
||||
|
||||
echo $db->fetchAffected($sql, $values);
|
||||
```
|
||||
|
||||
###### Ausgabe
|
||||
|
||||
```
|
||||
42
|
||||
```
|
||||
|
||||
## `lastInsertId()`
|
||||
|
||||
Gibt den zuletzt erzeugten Auto-Increment-Wert zurück.
|
||||
|
||||
```php
|
||||
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar)';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
// $db->fetchAffected($sql, $values); // Alternative
|
||||
|
||||
echo $db->lastInsertId();
|
||||
```
|
||||
|
||||
###### Ausgabe
|
||||
```
|
||||
123
|
||||
```
|
||||
|
||||
## Multiple-Row-Insert
|
||||
|
||||
```php
|
||||
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar1, :baz1), (NULL, :bar2, :baz2), (NULL, :bar3, :baz3)';
|
||||
|
||||
$values = [
|
||||
'bar1' => 'bar',
|
||||
'baz1' => 'baz',
|
||||
'bar2' => 'barbar',
|
||||
'baz2' => 'bazbaz',
|
||||
'bar3' => 'barbarbar',
|
||||
'baz3' => 'bazbazbaz',
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
// $db->fetchAffected($sql, $values); // Alternative; Rückgabe wäre `3`
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
# Datensätze abrufen
|
||||
|
||||
## `fetchAll()`
|
||||
|
||||
```php
|
||||
$data = $db->fetchAll('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => 6,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => 7,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchAssoc()`
|
||||
|
||||
Wie `fetchAll()` allerdings wird das Ergebnis der ersten Spalte als Index verwendet.
|
||||
|
||||
```php
|
||||
$data = $db->fetchAssoc('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
6 =>
|
||||
array (
|
||||
'id' => 6,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
),
|
||||
7 =>
|
||||
array (
|
||||
'id' => 7,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchRow()`
|
||||
|
||||
Liefert die Ergebnisse der ersten Zeile als assoziatives Array. Die Spaltennamen werden als Index verwendet.
|
||||
|
||||
```php
|
||||
$data = $db->fetchRow('SELECT a.id, a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
'id' => 6,
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
'name_en' => '',
|
||||
'logdatei' => '2015-10-26 17:26:27',
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchValue()`
|
||||
|
||||
Liefert nur das Ergebnis der ersten Zeile und Spalte zurück.
|
||||
|
||||
```php
|
||||
$data = $db->fetchValue('SELECT a.name_de, name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
'LED Anzeige RLED 24-8'
|
||||
```
|
||||
|
||||
## `fetchCol()`
|
||||
|
||||
Liefert nur die Ergebnisse der ersten Spalte zurück.
|
||||
|
||||
```php
|
||||
$data = $db->fetchCol('SELECT a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 => 'LED Anzeige RLED 24-8',
|
||||
1 => 'Schalter S3 24V 5A',
|
||||
2 => 'Gehäuse GHK5 20x30x10',
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchPairs()`
|
||||
|
||||
Gibt ein (eindimentionales) assoziatives Array zurück. Der Wert der ersten Spalte wird als Index verwendet und die zweite Spalte als Wert.
|
||||
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
|
||||
|
||||
```php
|
||||
$data = $db->fetchPairs('SELECT a.nummer, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
700006 => 'LED Anzeige RLED 24-8',
|
||||
700007 => 'Schalter S3 24V 5A',
|
||||
700008 => 'Gehäuse GHK5 20x30x10',
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchGroup()`
|
||||
|
||||
Verhält sich wie `fetchAssoc()`; mit der Ausnahme dass die Werte der ersten Spalte gruppiert werden.
|
||||
|
||||
```php
|
||||
$data = $db->fetchGroup(
|
||||
'SELECT a.typ, a.nummer, a.name_de, a.logdatei FROM artikel AS a WHERE a.id > 5'
|
||||
);
|
||||
```
|
||||
```
|
||||
array (
|
||||
'produkt' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700006',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
'logdatei' => '2015-10-26 17:26:27',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700007',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
'logdatei' => '2015-10-26 17:26:59',
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700008',
|
||||
'name_de' => 'Gehäuse GHK5 20x30x10',
|
||||
'logdatei' => '2018-05-23 05:18:52',
|
||||
),
|
||||
),
|
||||
'gebuehr' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'typ' => 'gebuehr',
|
||||
'nummer' => '100001',
|
||||
'name_de' => 'Versandkosten',
|
||||
'logdatei' => '2018-06-17 10:09:36',
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# Database-Komponente
|
||||
|
||||
###### Verwendete Libraries
|
||||
|
||||
* **Aura.SqlQuery**:
|
||||
* Composer: `aura/sqlquery:2.7.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sqlquery
|
||||
* GitHub: https://github.com/auraphp/Aura.SqlQuery
|
||||
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
|
||||
|
||||
* **~~Aura.Sql~~** Wird in 19.4 entfernt
|
||||
* Composer: `aura/sql:3.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sql
|
||||
* GitHub: https://github.com/auraphp/Aura.Sql
|
||||
* Docs: https://github.com/auraphp/Aura.Sql/blob/3.x/docs/index.md
|
||||
|
||||
##### Database-Komponente aus Container holen
|
||||
|
||||
```php
|
||||
$db = $container->get('Database');
|
||||
```
|
||||
|
||||
Im alten Bereich:
|
||||
|
||||
```php
|
||||
$db = $this->app->Container->get('Database');
|
||||
```
|
||||
|
||||
## Themen
|
||||
|
||||
* [Datensätze abrufen (fetch)](fetch_results.md)
|
||||
* [Datensätze abrufen mit Generatoren (yield)](yield_results.md)
|
||||
* [Datensätze ändern](data_manipulation.md)
|
||||
* [Transaktionen](transactions.md)
|
||||
* [Named Parameter / Prepared Statements](named_parameter.md)
|
||||
* [SQL Query Builder](query_builder.md)
|
||||
|
||||
## Exceptions
|
||||
|
||||
Die Database-Komponente verwendet intern `mysqli`. Im Unterschied zu `mysqli` werden in Fehlerfällen aber
|
||||
Exceptions geworfen; z.B.:
|
||||
|
||||
* Wenn die Verbindung zur Datenbank fehlschlägt => `ConnectionException`
|
||||
* Wenn ein SQL-Statement fehlerhaft ist oder aus anderen Gründen nicht erfolgreich ausgeführt
|
||||
werden kann => `QueryFailureException`
|
||||
* Wenn `Named Parameter` fehlen => `MissingParameterException`
|
||||
|
||||
##### ExceptionInterface
|
||||
|
||||
Alle Exceptions die von der Database-Komponente geworfen werden implementieren das
|
||||
`\Xentral\Components\Database\Exception\DatabaseExceptionInterface` Interface.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Named Parameter
|
||||
|
||||
`perform()`, `fetch*()` und `yield*()`-Methoden nehmen als zweiten Parameter ein assoziatives Array entgegen.
|
||||
Mit diesem Array werden lassen sich Werte als *Named Parameter* 'binden'.
|
||||
|
||||
```php
|
||||
$sql =
|
||||
'SELECT a.id, a.nummer, a.name_de
|
||||
FROM artikel AS a
|
||||
WHERE a.typ IN (:types)
|
||||
AND a.nummer LIKE :nummers
|
||||
LIMIT :start, :length';
|
||||
|
||||
$values = [
|
||||
'types' => ['produkt', 'gebuehr'],
|
||||
'nummers' => '7000%',
|
||||
'start' => 0,
|
||||
'length' => 3,
|
||||
];
|
||||
|
||||
$data = $db->fetchAll($sql, $values);
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => 1,
|
||||
'nummer' => '700001',
|
||||
'name_de' => 'Schraube M10x20',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => 2,
|
||||
'nummer' => '700002',
|
||||
'name_de' => 'Sechskant-Mutter M10',
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'id' => 3,
|
||||
'nummer' => '700003',
|
||||
'name_de' => 'Schalthebel 20x10',
|
||||
),
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# Profiler
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @var \Xentral\Components\Database\Profiler\Profiler $profiler */
|
||||
$profiler = $container->get('DatabaseProfiler');
|
||||
$profiler->setActive(true);
|
||||
|
||||
$database->fetchAll($sql);
|
||||
|
||||
var_dump($profiler->getContexts());
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# SQL Query Builder
|
||||
|
||||
###### Verwendete Libraries
|
||||
|
||||
* **Aura.SqlQuery**:
|
||||
* Composer: `aura/sqlquery:2.7.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sqlquery
|
||||
* GitHub: https://github.com/auraphp/Aura.SqlQuery
|
||||
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
|
||||
|
||||
## Query Builder erzeugen
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$update = $db->update();
|
||||
$insert = $db->insert();
|
||||
$delete = $db->delete();
|
||||
```
|
||||
|
||||
## SELECT-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#select
|
||||
|
||||
###### Beispiel mit Named-Placeholder
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.id = :user_id')
|
||||
->bindValue('user_id', 1);
|
||||
|
||||
$result = $db->fetchAll(
|
||||
$select->getStatement(),
|
||||
$select->getBindValues()
|
||||
);
|
||||
|
||||
var_export($result);
|
||||
// array(
|
||||
// 0 => array(
|
||||
// 'id' => 1,
|
||||
// 'description' => 'Administrator',
|
||||
// ),
|
||||
// )
|
||||
```
|
||||
|
||||
###### Alternative mit ?-Placeholder
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.id = ?', 1);
|
||||
```
|
||||
|
||||
###### Beispiel mit Verschachtelung im WHERE
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.activ = ?', 1)
|
||||
->where(function (SelectQuery $query) {
|
||||
$query->where('u.type = ?', 'admin')
|
||||
->orWhere('u.type = ?', 'standard');
|
||||
});
|
||||
|
||||
echo $select->getStatement();
|
||||
```
|
||||
```sql
|
||||
SELECT
|
||||
`u`.`id`,
|
||||
`u`.`description`
|
||||
FROM
|
||||
`user` AS `u`
|
||||
WHERE
|
||||
`u`.`activ` = :_1_1_
|
||||
AND (
|
||||
`u`.`type` = :_1_2_
|
||||
OR `u`.`type` = :_1_3_
|
||||
)
|
||||
```
|
||||
|
||||
## INSERT-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#insert
|
||||
|
||||
## UPDATE-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#update
|
||||
|
||||
## DELETE-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#delete
|
||||
@@ -0,0 +1,25 @@
|
||||
# Transaktionnen
|
||||
|
||||
## Transaktion starten
|
||||
|
||||
```php
|
||||
$db->beginTransaction();
|
||||
```
|
||||
|
||||
## Transaktion übernehmen / Commit
|
||||
|
||||
```php
|
||||
$db->commit();
|
||||
```
|
||||
|
||||
## Transaktion zurücknehmen / Rollback
|
||||
|
||||
```php
|
||||
$db->rollback();
|
||||
```
|
||||
|
||||
## Prüfen ob Transaktion gestartet ist
|
||||
|
||||
```php
|
||||
$db->inTransaction();
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
|
||||
# Datensätze abrufen mit Generatoren
|
||||
|
||||
Um den Arbeitsspeicherverbrauch gering zu halten bieten sich zum Iterieren von großen Datenmengen
|
||||
Generatoren an: https://www.php.net/manual/de/language.generators.overview.php
|
||||
|
||||
Die Database-Komponente stellt für diesen Zweck `yield`-Methoden zur Verfügung.
|
||||
|
||||
|
||||
## `yieldAll()`
|
||||
|
||||
Wie `fetchAll()`; jede Zeile ist ein assoziatives Array.
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldAll($statement, $bindValues) as $row) {
|
||||
var_dump($row);
|
||||
}
|
||||
```
|
||||
```
|
||||
array (size=3)
|
||||
'id' => int 2
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
array (size=3)
|
||||
'id' => int 3
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldAssoc()`
|
||||
|
||||
Wie `fetchAssoc()`; jede Zeile ist ein assoziatives Array; der Key beinhaltet den Wert der ersten Spalte
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldAssoc($statement, $bindValues) as $key => $row) {
|
||||
var_dump($key);
|
||||
var_dump($row);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 2
|
||||
array (size=3)
|
||||
'id' => int 2
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 3
|
||||
array (size=3)
|
||||
'id' => int 3
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldPairs()`
|
||||
|
||||
Wie `fetchPairs()`; jede Zeile besteht aus Key-Value-Paaren; der Key beinhaltet den Inhalt der ersten Spalte;
|
||||
der Wert den Inhalt der zweiten Spalte.
|
||||
|
||||
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldPairs($statement, $bindValues) as $key => $value) {
|
||||
var_dump($key);
|
||||
var_dump($value);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 2
|
||||
string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 3
|
||||
string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldCol()`
|
||||
|
||||
Wie `fetchCol()`; jede Zeile beinhaltet nur den Wert der ersten Spalte.
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldCol($statement, $bindValues) as $key => $value) {
|
||||
var_dump($key);
|
||||
var_dump($value);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 0
|
||||
string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 1
|
||||
string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Components\EnvironmentConfig;
|
||||
|
||||
use SplFileInfo;
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Config;
|
||||
use License;
|
||||
use Xentral\Modules\Api\LegacyBridge\LegacyApplication;
|
||||
|
||||
class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices(): array
|
||||
{
|
||||
return [
|
||||
'EnvironmentConfig' => 'onInitEnvironmentConfig',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return EnvironmentConfig
|
||||
*/
|
||||
public static function onInitEnvironmentConfig(ContainerInterface $container): EnvironmentConfig
|
||||
{
|
||||
$provider = self::onInitEnvironmentConfigProvider($container);
|
||||
|
||||
return $provider->createEnvironmentConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return EnvironmentConfigProvider
|
||||
*/
|
||||
private static function onInitEnvironmentConfigProvider(ContainerInterface $container): EnvironmentConfigProvider
|
||||
{
|
||||
/** @var LegacyApplication $app */
|
||||
$app = $container->get('LegacyApplication');
|
||||
|
||||
if (!class_exists('License')) {
|
||||
$test = dirname(__DIR__, 3) . '/phpwf/plugins/class.license.php';
|
||||
$file = new SplFileInfo($test);
|
||||
if ($file->isFile()) {
|
||||
include $test;
|
||||
}
|
||||
}
|
||||
|
||||
return new EnvironmentConfigProvider(new License(), $app->Conf);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\EnvironmentConfig;
|
||||
|
||||
final class EnvironmentConfig
|
||||
{
|
||||
/**
|
||||
* @var string $databaseHost
|
||||
*/
|
||||
private $databaseHost;
|
||||
|
||||
/**
|
||||
* @var string $databaseName
|
||||
*/
|
||||
private $databaseName;
|
||||
|
||||
/**
|
||||
* @var string $databaseUser
|
||||
*/
|
||||
private $databaseUser;
|
||||
|
||||
/**
|
||||
* @var string $databasePassword
|
||||
*/
|
||||
private $databasePassword;
|
||||
|
||||
/**
|
||||
* @var int $databasePort
|
||||
*/
|
||||
private $databasePort;
|
||||
|
||||
/**
|
||||
* @var string $userdataDirectoryPath
|
||||
*/
|
||||
private $userdataDirectoryPath;
|
||||
|
||||
/**
|
||||
* @var array|null $ioncubeSystemInformation
|
||||
*/
|
||||
private $ioncubeSystemInformation;
|
||||
|
||||
/**
|
||||
* @param string $databaseHost
|
||||
* @param string $databaseName
|
||||
* @param string $databaseUser
|
||||
* @param string $databasePassword
|
||||
* @param int $databasePort
|
||||
* @param string $userdataDirectoryPath
|
||||
* @param array $ioncubeSystemInformation
|
||||
*/
|
||||
public function __construct(
|
||||
string $databaseHost,
|
||||
string $databaseName,
|
||||
string $databaseUser,
|
||||
string $databasePassword,
|
||||
int $databasePort,
|
||||
string $userdataDirectoryPath,
|
||||
?array $ioncubeSystemInformation
|
||||
) {
|
||||
$this->databaseHost = $databaseHost;
|
||||
$this->databaseName = $databaseName;
|
||||
$this->databaseUser = $databaseUser;
|
||||
$this->databasePassword = $databasePassword;
|
||||
$this->databasePort = $databasePort;
|
||||
$this->userdataDirectoryPath = $userdataDirectoryPath;
|
||||
$this->ioncubeSystemInformation = $ioncubeSystemInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabaseHost(): string
|
||||
{
|
||||
return $this->databaseHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabaseName(): string
|
||||
{
|
||||
return $this->databaseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabaseUser(): string
|
||||
{
|
||||
return $this->databaseUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabasePassword(): string
|
||||
{
|
||||
return $this->databasePassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDatabasePort(): int
|
||||
{
|
||||
return $this->databasePort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUserdataDirectoryPath(): string
|
||||
{
|
||||
return $this->userdataDirectoryPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array
|
||||
*/
|
||||
public function getIoncubeSystemInformation(): ?array
|
||||
{
|
||||
return $this->ioncubeSystemInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSystemHostedOnCloud(): bool
|
||||
{
|
||||
return !empty($this->ioncubeSystemInformation['iscloud']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSystemFlaggedAsDevelopmentVersion(): bool
|
||||
{
|
||||
return !empty($this->ioncubeSystemInformation['isdevelopmentversion']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSystemFlaggedAsTestVersion(): bool
|
||||
{
|
||||
return !empty($this->ioncubeSystemInformation['testlizenz']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxUser(): int
|
||||
{
|
||||
if (!isset($this->ioncubeSystemInformation['maxuser']['value'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)$this->ioncubeSystemInformation['maxuser']['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxLightUser(): int
|
||||
{
|
||||
if (!isset($this->ioncubeSystemInformation['maxlightuser']['value'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)$this->ioncubeSystemInformation['maxlightuser']['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getExpirationTimeStamp(): ?int
|
||||
{
|
||||
if (!isset($this->ioncubeSystemInformation['expdate']['value'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)$this->ioncubeSystemInformation['expdate']['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValueOfSpecificIoncubeSystemInformation(string $name): ?string
|
||||
{
|
||||
if ($this->ioncubeSystemInformation === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $this->ioncubeSystemInformation)) {
|
||||
return $this->ioncubeSystemInformation[$name]['value'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSystemFallbackEmailAddresses(): array
|
||||
{
|
||||
$emailAddresses = [];
|
||||
$mailAddressSelfBuyCustomer = (string)$this->getValueOfSpecificIoncubeSystemInformation('buyemail');
|
||||
if ($mailAddressSelfBuyCustomer !== '') {
|
||||
$emailAddresses[] = $mailAddressSelfBuyCustomer;
|
||||
}
|
||||
|
||||
$mailAddressCustomerLicence = (string)$this->getValueOfSpecificIoncubeSystemInformation('emaillicence');
|
||||
if ($mailAddressCustomerLicence !== ''
|
||||
&& strpos($mailAddressCustomerLicence, '@') !== false
|
||||
&& strpos($mailAddressCustomerLicence, '@xentral.com') === false
|
||||
&& strpos($mailAddressCustomerLicence, '@xentral.biz') === false) {
|
||||
$emailAddresses[] = $mailAddressCustomerLicence;
|
||||
}
|
||||
|
||||
//in old licences email-address of customer can be insert in name instead email
|
||||
$nameCustomerLicence = (string)$this->getValueOfSpecificIoncubeSystemInformation('namelicence');
|
||||
if ($nameCustomerLicence !== ''
|
||||
&& strpos($nameCustomerLicence, '@') !== false
|
||||
&& strpos($nameCustomerLicence, '@xentral.com') === false
|
||||
&& strpos($nameCustomerLicence, '@xentral.biz') === false) {
|
||||
$emailAddresses[] = $nameCustomerLicence;
|
||||
}
|
||||
|
||||
return $emailAddresses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\EnvironmentConfig;
|
||||
|
||||
use Config;
|
||||
use License;
|
||||
|
||||
final class EnvironmentConfigProvider
|
||||
{
|
||||
/** @var License $license */
|
||||
private $license;
|
||||
|
||||
/** @var Config $config */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @param License $license
|
||||
* @param Config $config
|
||||
*/
|
||||
public function __construct(License $license, Config $config)
|
||||
{
|
||||
$this->license = $license;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EnvironmentConfig
|
||||
*/
|
||||
public function createEnvironmentConfig(): EnvironmentConfig
|
||||
{
|
||||
$environmentConfig = new EnvironmentConfig(
|
||||
$this->config->WFdbhost, $this->config->WFdbname, $this->config->WFdbuser,
|
||||
$this->config->WFdbpass, $this->config->WFdbport, $this->config->WFuserdata,
|
||||
(array)$this->license->getProperties()
|
||||
);
|
||||
|
||||
return $environmentConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Collection;
|
||||
|
||||
use ArrayObject;
|
||||
use AppendIterator;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
|
||||
|
||||
final class DataCollection implements Iterator
|
||||
{
|
||||
/** @var AppendIterator $data */
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @param array|Iterator $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(...$data)
|
||||
{
|
||||
$this->data = new AppendIterator();
|
||||
foreach ($data as $item) {
|
||||
$this->append($item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Iterator|IteratorAggregate $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function append($data)
|
||||
{
|
||||
$type = gettype($data);
|
||||
if ($type === 'object') {
|
||||
$type = get_class($data);
|
||||
if ($data instanceof Iterator) {
|
||||
$type = 'Iterator';
|
||||
}
|
||||
if ($data instanceof IteratorAggregate) {
|
||||
$type = 'IteratorAggregate';
|
||||
}
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'array':
|
||||
$this->data->append((new ArrayObject($data))->getIterator());
|
||||
break;
|
||||
|
||||
case 'Iterator':
|
||||
$this->data->append($data);
|
||||
break;
|
||||
|
||||
case 'IteratorAggregate':
|
||||
$this->data->append($data->getIterator());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @return mixed Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->data->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->data->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->data->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @return boolean Returns true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->data->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->data->rewind();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Collection;
|
||||
|
||||
use ArrayObject;
|
||||
use Closure;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Exporter\Exception\InvalidReturnTypeException;
|
||||
|
||||
final class FormatterCollection implements Iterator
|
||||
{
|
||||
/** @var Iterator $data */
|
||||
private $data;
|
||||
|
||||
/** @var callable $callback */
|
||||
private $callback;
|
||||
|
||||
/**
|
||||
* @param array|Iterator $data
|
||||
* @param callable|Closure $callback
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($data, $callback)
|
||||
{
|
||||
if (!is_callable($callback, false)) {
|
||||
throw new InvalidArgumentException('Callback is not callable');
|
||||
}
|
||||
$this->callback = $callback;
|
||||
|
||||
$type = gettype($data);
|
||||
if ($type === 'object') {
|
||||
$type = get_class($data);
|
||||
if ($data instanceof Iterator) {
|
||||
$type = 'Iterator';
|
||||
}
|
||||
if ($data instanceof IteratorAggregate) {
|
||||
$type = 'IteratorAggregate';
|
||||
}
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'array':
|
||||
$this->data = (new ArrayObject($data))->getIterator();
|
||||
break;
|
||||
|
||||
case 'Iterator':
|
||||
$this->data = $data;
|
||||
break;
|
||||
|
||||
case 'IteratorAggregate':
|
||||
$this->data = $data->getIterator();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @throws InvalidReturnTypeException
|
||||
*
|
||||
* @return mixed Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$result = call_user_func($this->callback, $this->data->current());
|
||||
|
||||
if (!is_array($result)) {
|
||||
throw new InvalidReturnTypeException('Formatter return type is invalid . Callable must return an array.');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->data->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->data->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @return boolean Returns true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->data->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->data->rewind();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Csv;
|
||||
|
||||
final class CsvConfig
|
||||
{
|
||||
/** @var string $delimiter */
|
||||
private $delimiter;
|
||||
|
||||
/** @var string $enclosure */
|
||||
private $enclosure;
|
||||
|
||||
/** @var string $escapeChar */
|
||||
private $escapeChar;
|
||||
|
||||
/** @var string $sourceCharset */
|
||||
private $sourceCharset;
|
||||
|
||||
/** @var string $targetCharset */
|
||||
private $targetCharset;
|
||||
|
||||
/** @var bool $forceEnclosureEnabled */
|
||||
private $forceEnclosureEnabled;
|
||||
|
||||
/**
|
||||
* @param string $delimiter
|
||||
* @param string $enclosure
|
||||
* @param string $escapeChar
|
||||
* @param string $targetCharset
|
||||
* @param string $sourceCharset
|
||||
* @param bool $forceEnclosureEnabled
|
||||
*/
|
||||
public function __construct(
|
||||
$delimiter = ',',
|
||||
$enclosure = '"',
|
||||
$escapeChar = "\\",
|
||||
$targetCharset = 'UTF-8',
|
||||
$sourceCharset = 'UTF-8',
|
||||
$forceEnclosureEnabled = false
|
||||
) {
|
||||
$this->delimiter = $delimiter;
|
||||
$this->enclosure = $enclosure;
|
||||
$this->escapeChar = $escapeChar;
|
||||
$this->targetCharset = $targetCharset;
|
||||
$this->sourceCharset = $sourceCharset;
|
||||
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDelimiter()
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEnclosure()
|
||||
{
|
||||
return $this->enclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEscapeChar()
|
||||
{
|
||||
return $this->escapeChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSourceCharset()
|
||||
{
|
||||
return $this->sourceCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTargetCharset()
|
||||
{
|
||||
return $this->targetCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $delimiter
|
||||
*/
|
||||
public function setDelimiter($delimiter)
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $enclosure
|
||||
*/
|
||||
public function setEnclosure($enclosure)
|
||||
{
|
||||
$this->enclosure = $enclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $escapeChar
|
||||
*/
|
||||
public function setEscapeChar($escapeChar)
|
||||
{
|
||||
$this->escapeChar = $escapeChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sourceCharset
|
||||
*/
|
||||
public function setSourceCharset($sourceCharset)
|
||||
{
|
||||
$this->sourceCharset = $sourceCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $targetCharset
|
||||
*/
|
||||
public function setTargetCharset($targetCharset)
|
||||
{
|
||||
$this->targetCharset = $targetCharset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isForceEnclosureEnabled()
|
||||
{
|
||||
return $this->forceEnclosureEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $forceEnclosureEnabled
|
||||
*/
|
||||
public function setForceEnclosureEnabled($forceEnclosureEnabled)
|
||||
{
|
||||
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Csv;
|
||||
|
||||
use Iterator;
|
||||
use Xentral\Components\Exporter\Exception\FileExistsException;
|
||||
use Xentral\Components\Exporter\Exception\InvalidResourceException;
|
||||
|
||||
final class CsvExporter
|
||||
{
|
||||
/** @var CsvConfig $config */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @param CsvConfig|null $config
|
||||
*/
|
||||
public function __construct(CsvConfig $config = null)
|
||||
{
|
||||
if ($config === null) {
|
||||
$config = new CsvConfig();
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath Resource used for writing
|
||||
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
|
||||
*
|
||||
* @throws FileExistsException|InvalidResourceException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function export($filePath, $data)
|
||||
{
|
||||
$resource = $this->exportToResource($filePath, $data);
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as ::export() beside that the created resource will be returned
|
||||
*
|
||||
* @param string $filePath Resource used for writing
|
||||
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
|
||||
*
|
||||
* @throws FileExistsException|InvalidResourceException
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public function exportToResource($filePath, $data)
|
||||
{
|
||||
if (is_file($filePath)) {
|
||||
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
|
||||
}
|
||||
|
||||
// 'x+' = Create and open for reading and writing.
|
||||
// File pointer will be placed at the beginning of the file.
|
||||
// If the file already exists `fopen` will return false.
|
||||
// 'b' = Enable binary mode
|
||||
$resource = @fopen($filePath, 'x+b');
|
||||
if ($resource === false) {
|
||||
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
|
||||
}
|
||||
|
||||
$writer = new CsvWriter($resource, $this->config);
|
||||
$writer->writeLines($data);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Csv;
|
||||
|
||||
use Iterator;
|
||||
use Xentral\Components\Exporter\Exception\InvalidResourceException;
|
||||
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
|
||||
|
||||
final class CsvWriter
|
||||
{
|
||||
/** @var resource $handle */
|
||||
private $handle;
|
||||
|
||||
/** @var CsvConfig $config */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
* @param CsvConfig|null $config
|
||||
*
|
||||
* @throws InvalidResourceException If resource is not writable or invalid
|
||||
* @throws PhpExtensionMissingException If mbstring is missing
|
||||
*/
|
||||
public function __construct($handle, CsvConfig $config = null)
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
throw new InvalidResourceException('First parameter is not a valid resource.');
|
||||
}
|
||||
if (!$this->isStreamWritable($handle)) {
|
||||
throw new InvalidResourceException('Resource is not writable.');
|
||||
}
|
||||
if (!function_exists('mb_convert_encoding')) {
|
||||
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
|
||||
}
|
||||
if ($config === null) {
|
||||
$config = new CsvConfig();
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
$this->handle = $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Iterator $lines
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function writeLines($lines)
|
||||
{
|
||||
foreach ($lines as $line) {
|
||||
$this->writeLine($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $line
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function writeLine($line)
|
||||
{
|
||||
if ($this->config->isForceEnclosureEnabled()) {
|
||||
$fields = $this->encloseAllValues($line);
|
||||
fwrite(
|
||||
$this->handle,
|
||||
sprintf(
|
||||
"%s\n",
|
||||
implode($this->config->getDelimiter(), $this->convertCharset($fields))
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fputcsv(
|
||||
$this->handle,
|
||||
$this->convertCharset($line),
|
||||
$this->config->getDelimiter(),
|
||||
$this->config->getEnclosure(),
|
||||
$this->config->getEscapeChar()
|
||||
);
|
||||
}
|
||||
|
||||
/***
|
||||
* @param array $line
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function encloseAllValues($line)
|
||||
{
|
||||
$escapeChar = $this->config->getEscapeChar();
|
||||
$enclosure = $this->config->getEnclosure();
|
||||
$result = [];
|
||||
foreach ($line as $key => $value) {
|
||||
$value = str_replace($escapeChar . $enclosure, $enclosure, $value);
|
||||
$value = str_replace($enclosure, $escapeChar . $enclosure, $value);
|
||||
$result[$key] = sprintf('%1$s%2$s%1$s', $enclosure, $value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $line
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function convertCharset($line)
|
||||
{
|
||||
if ($this->config->getSourceCharset() === $this->config->getTargetCharset()) {
|
||||
return $line; // No conversion needed
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($line as $key => $cellData) {
|
||||
$result[$key] = mb_convert_encoding(
|
||||
$cellData,
|
||||
$this->config->getTargetCharset(),
|
||||
$this->config->getSourceCharset()
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isStreamWritable($handle)
|
||||
{
|
||||
$meta = stream_get_meta_data($handle);
|
||||
$currentMode = $meta['mode'];
|
||||
|
||||
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
|
||||
foreach ($writeModes as $writeMode) {
|
||||
if (strpos($currentMode, $writeMode) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface ExporterExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileExistsException extends RuntimeException implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidJsonException extends Exception implements ExporterExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @param int $errorCode
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function fromJsonError($errorCode)
|
||||
{
|
||||
$exception = new self(self::mapJsonError($errorCode));
|
||||
return $exception;
|
||||
}
|
||||
|
||||
private static function mapJsonError($jsonError)
|
||||
{
|
||||
switch ($jsonError) {
|
||||
case JSON_ERROR_NONE:
|
||||
$msg = 'Unknown error';
|
||||
break;
|
||||
case JSON_ERROR_DEPTH:
|
||||
$msg = 'The maximum stack depth has been exceeded';
|
||||
break;
|
||||
case JSON_ERROR_STATE_MISMATCH:
|
||||
$msg = 'Invalid or malformed JSON';
|
||||
break;
|
||||
case JSON_ERROR_CTRL_CHAR:
|
||||
$msg = 'Control character error, possibly incorrectly encoded';
|
||||
break;
|
||||
case JSON_ERROR_SYNTAX:
|
||||
$msg = 'Syntax error';
|
||||
break;
|
||||
case JSON_ERROR_UTF8:
|
||||
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
|
||||
break;
|
||||
case JSON_ERROR_RECURSION:
|
||||
$msg = 'One or more recursive references in the value to be encoded';
|
||||
break;
|
||||
case JSON_ERROR_INF_OR_NAN:
|
||||
$msg = 'One or more NAN or INF values in the value to be encoded';
|
||||
break;
|
||||
case JSON_ERROR_UNSUPPORTED_TYPE:
|
||||
$msg = 'A value of a type that cannot be encoded was given';
|
||||
break;
|
||||
case JSON_ERROR_INVALID_PROPERTY_NAME:
|
||||
$msg = 'A property name that cannot be encoded was given';
|
||||
break;
|
||||
case JSON_ERROR_UTF16:
|
||||
$msg = 'Malformed UTF-16 characters, possibly incorrectly encoded';
|
||||
break;
|
||||
default:
|
||||
$msg = 'Unknown Error';
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class InvalidResourceException extends RuntimeException implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class InvalidReturnTypeException extends LogicException implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class PhpExtensionMissingException extends RuntimeException implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ResourceWriteException extends Exception implements ExporterExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Json;
|
||||
|
||||
final class JsonConfig
|
||||
{
|
||||
/** @var int $options */
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @see https://www.php.net/manual/en/json.constants.php
|
||||
*
|
||||
* @param int $options
|
||||
*/
|
||||
public function __construct($options = 0)
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Json;
|
||||
|
||||
use Xentral\Components\Exporter\Exception\FileExistsException;
|
||||
use Xentral\Components\Exporter\Exception\InvalidResourceException;
|
||||
|
||||
final class JsonExporter
|
||||
{
|
||||
/** @var JsonConfig $config */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @param JsonConfig|null $config
|
||||
*/
|
||||
public function __construct(JsonConfig $config = null)
|
||||
{
|
||||
if ($config === null) {
|
||||
$config = new JsonConfig();
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filePath Resource used for writing
|
||||
* @param array $data Multi-dimentional array
|
||||
*
|
||||
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
|
||||
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
|
||||
* @return void
|
||||
*/
|
||||
public function export($filePath, $data)
|
||||
{
|
||||
$resource = $this->exportToResource($filePath, $data);
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as ::export() beside that the created resource will be returned
|
||||
*
|
||||
* @param string $filePath Resource used for writing
|
||||
* @param array $data Multi-dimentional array
|
||||
*
|
||||
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
|
||||
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
|
||||
* @return resource
|
||||
*/
|
||||
public function exportToResource($filePath, $data)
|
||||
{
|
||||
if (is_file($filePath)) {
|
||||
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
|
||||
}
|
||||
|
||||
// 'x+' = Create and open for reading and writing.
|
||||
// File pointer will be placed at the beginning of the file.
|
||||
// If the file already exists `fopen` will return false.
|
||||
// 'b' = Enable binary mode
|
||||
$resource = @fopen($filePath, 'x+b');
|
||||
if ($resource === false) {
|
||||
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
|
||||
}
|
||||
|
||||
$writer = new JsonWriter($resource, $this->config);
|
||||
$writer->write($data);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Exporter\Json;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Components\Exporter\Exception\InvalidJsonException;
|
||||
use Xentral\Components\Exporter\Exception\InvalidResourceException;
|
||||
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
|
||||
use Xentral\Components\Exporter\Exception\ResourceWriteException;
|
||||
|
||||
final class JsonWriter
|
||||
{
|
||||
/** @var resource $handle */
|
||||
private $handle;
|
||||
|
||||
/** @var JsonConfig $config */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
* @param JsonConfig|null $config
|
||||
*
|
||||
* @throws InvalidResourceException If resource is not writable or invalid
|
||||
* @throws PhpExtensionMissingException If mbstring is missing
|
||||
*/
|
||||
public function __construct($handle, JsonConfig $config = null)
|
||||
{
|
||||
if (!is_resource($handle)) {
|
||||
throw new InvalidResourceException('First parameter is not a valid resource.');
|
||||
}
|
||||
if (!$this->isStreamWritable($handle)) {
|
||||
throw new InvalidResourceException('Resource is not writable.');
|
||||
}
|
||||
if (!function_exists('mb_convert_encoding')) {
|
||||
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
|
||||
}
|
||||
if ($config === null) {
|
||||
$config = new JsonConfig();
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
$this->handle = $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|JsonSerializable $data
|
||||
*
|
||||
* @throws PhpExtensionMissingException If json is missing
|
||||
* @throws ResourceWriteException
|
||||
* @throws InvalidJsonException
|
||||
*/
|
||||
public function write($data)
|
||||
{
|
||||
if (!function_exists('json_encode')) {
|
||||
throw new PhpExtensionMissingException('Required PHP extension "json" is missing.');
|
||||
}
|
||||
|
||||
$jsonOptions = $this->config->getOptions();
|
||||
$jsonString = @json_encode($data, $jsonOptions);
|
||||
|
||||
if ($jsonString === false) {
|
||||
throw InvalidJsonException::fromJsonError(json_last_error());
|
||||
}
|
||||
|
||||
$writeResult = @fwrite($this->handle, $jsonString);
|
||||
if ($writeResult === false) {
|
||||
throw new ResourceWriteException("JSON could not be written to resource.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isStreamWritable($handle)
|
||||
{
|
||||
$meta = stream_get_meta_data($handle);
|
||||
$currentMode = $meta['mode'];
|
||||
|
||||
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
|
||||
foreach ($writeModes as $writeMode) {
|
||||
if (strpos($currentMode, $writeMode) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# CSV-Exporter
|
||||
|
||||
## Datei erstellen
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Components\Exporter\Csv\CsvExporter;
|
||||
|
||||
/** @var Database $db */
|
||||
$db = $container->get('Database');
|
||||
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
|
||||
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'employees') . '.csv';
|
||||
|
||||
$exporter = new CsvExporter();
|
||||
$exporter->export($filePath, $data);
|
||||
```
|
||||
|
||||
##### Rückgabewerte
|
||||
|
||||
* Die `export()` Methode liefert nichts zurück.
|
||||
* Die `exportToResource()` gibt die geöffnete Ressource zurück. Die Ressource muss manuell (mit `fclose()`)
|
||||
geschlossen werden. Der Dateizeiger ist auf `EOF` platziert.
|
||||
|
||||
##### Funktionsparameter
|
||||
|
||||
Beide `export*()`-Methoden nehmen die gleichen Parameter entgegen.
|
||||
|
||||
* Als erster Parameter wird ein absoluter Dateipfad erwartet. Alternativ kann ein Stream Wrapper angegeben werden.
|
||||
Beispiele:
|
||||
* `php://output` um die CSV direkt auszugeben (`echo`).
|
||||
* `php://memory` um in den Arbeitsspeicher zu schreiben.
|
||||
* `php://temp` um in eine temporäre Datei zu schreiben.
|
||||
|
||||
* Der zweite Parameter nimmt die Daten entgegen die als CSV exportiert werden sollen. Folgende Typen sind erlaubt:
|
||||
* `array`
|
||||
* `Generator`
|
||||
* `Iterator`
|
||||
* `IteratorAggregate`
|
||||
|
||||
##### Konstruktor-Parameter
|
||||
|
||||
* Der erste Konstruktor-Parameter ist optional und nimmt die CSV-Konfiguration entgegen;
|
||||
siehe _Erweiterte Beispiele_ > _CSV konfigurieren_.
|
||||
|
||||
## Datei-Download erstellen
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Components\Exporter\Csv\CsvExporter;
|
||||
|
||||
/** @var Database $db */
|
||||
$db = $container->get('Database');
|
||||
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
|
||||
|
||||
$exporter = new CsvExporter();
|
||||
$resource = $exporter->exportToResource('php://memory', $data);
|
||||
rewind($resource);
|
||||
$stat = fstat($resource);
|
||||
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: must-revalidate');
|
||||
header('Content-type: text/csv');
|
||||
header('Content-Disposition: attachment; filename="employees.csv"');
|
||||
header('Content-Length: ' . $stat['size']);
|
||||
fpassthru($resource);
|
||||
fclose($resource);
|
||||
```
|
||||
|
||||
|
||||
## Erweiterte Beispiele
|
||||
|
||||
### CSV konfigurieren
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Xentral\Components\Exporter\Csv\CsvConfig;
|
||||
use Xentral\Components\Exporter\Csv\CsvExporter;
|
||||
|
||||
$csvConfig = new CsvConfig();
|
||||
$csvConfig->setDelimiter(';');
|
||||
$csvConfig->setEnclosure('"');
|
||||
$csvConfig->setSourceCharset('UTF-8');
|
||||
$csvConfig->setTargetCharset('ISO-8859-1');
|
||||
|
||||
$exporter = new CsvExporter($csvConfig);
|
||||
$exporter->export($filePath, $data);
|
||||
```
|
||||
|
||||
### Daten zusammenführen
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Xentral\Components\Exporter\Csv\CsvExporter;
|
||||
use Xentral\Components\Exporter\Collection\DataCollection;
|
||||
|
||||
$collection = new DataCollection($headline, $employees);
|
||||
|
||||
$exporter = new CsvExporter();
|
||||
$exporter->export($filePath, $collection);
|
||||
```
|
||||
|
||||
### Daten formatieren
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Xentral\Components\Exporter\Csv\CsvExporter;
|
||||
use Xentral\Components\Exporter\Collection\FormatterCollection;
|
||||
|
||||
$formatter = new FormatterCollection($data, function ($row) {
|
||||
$row['fullname'] = $row['firstname'] . ' ' . $row['lastname'];
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
$exporter = new CsvExporter();
|
||||
$exporter->export($filePath, $formatter);
|
||||
```
|
||||
|
||||
Statt einer anonymen Funktion (`Closure`) kann ein `callable` übergeben werden.
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Adapter;
|
||||
|
||||
interface AdapterInterface extends ReaderAdapterInterface
|
||||
{
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write($path, $contents, array $config = []);
|
||||
|
||||
/**
|
||||
* Creates a new file using a stream
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeStream($path, $resource, array $config = []);
|
||||
|
||||
/**
|
||||
* Updates an existing file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update($path, $contents, array $config = []);
|
||||
|
||||
/**
|
||||
* Updates an existing file using a stream
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateStream($path, $resource, array $config = []);
|
||||
|
||||
/**
|
||||
* Renames a file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function rename($path, $newpath);
|
||||
|
||||
/**
|
||||
* Copies a file to new location
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function copy($path, $newpath);
|
||||
|
||||
/**
|
||||
* Deletes a single file
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($path);
|
||||
|
||||
/**
|
||||
* Deletes a directory and its contents
|
||||
*
|
||||
* @param string $directory
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteDir($directory);
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createDir($directory, array $config = []);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Adapter;
|
||||
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
|
||||
final class FtpConfig
|
||||
{
|
||||
/** @var string $hostname */
|
||||
private $hostname;
|
||||
|
||||
/** @var string $username */
|
||||
private $username;
|
||||
|
||||
/** @var string $password */
|
||||
private $password;
|
||||
|
||||
/** @var string $rootDir */
|
||||
private $rootDir;
|
||||
|
||||
/** @var int $port */
|
||||
private $port;
|
||||
|
||||
/** @var int $timeout */
|
||||
private $timeout;
|
||||
|
||||
/** @var bool $passive */
|
||||
private $passive;
|
||||
|
||||
/** @var bool $ssl */
|
||||
private $ssl;
|
||||
|
||||
/**
|
||||
* @param string $hostname
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $rootDir
|
||||
* @param int $port
|
||||
* @param int $timeout
|
||||
* @param bool $passive
|
||||
* @param bool $ssl
|
||||
*/
|
||||
public function __construct(
|
||||
$hostname,
|
||||
$username,
|
||||
$password,
|
||||
$rootDir = '/',
|
||||
$port = 21,
|
||||
$timeout = 30,
|
||||
$passive = true,
|
||||
$ssl = false
|
||||
) {
|
||||
if (empty($hostname)) {
|
||||
throw new InvalidArgumentException('Hostname is empty.');
|
||||
}
|
||||
if (empty($username)) {
|
||||
throw new InvalidArgumentException('Username is empty.');
|
||||
}
|
||||
if (empty($password)) {
|
||||
throw new InvalidArgumentException('Password is empty.');
|
||||
}
|
||||
if (empty($rootDir)) {
|
||||
throw new InvalidArgumentException('Root dir is empty.');
|
||||
}
|
||||
|
||||
$this->hostname = (string)$hostname;
|
||||
$this->username = (string)$username;
|
||||
$this->password = (string)$password;
|
||||
$this->rootDir = (string)$rootDir;
|
||||
$this->port = (int)$port;
|
||||
$this->timeout = (int)$timeout;
|
||||
$this->passive = (bool)$passive;
|
||||
$this->ssl = (bool)$ssl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'host' => $this->hostname,
|
||||
'username' => $this->username,
|
||||
'password' => $this->password,
|
||||
'root' => $this->rootDir,
|
||||
'port' => $this->port,
|
||||
'timeout' => $this->timeout,
|
||||
'passive' => $this->passive,
|
||||
'ssl' => $this->ssl,
|
||||
'recurseManually' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
*/
|
||||
public function getRootDir()
|
||||
{
|
||||
return $this->rootDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimeout()
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPassive()
|
||||
{
|
||||
return $this->passive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSsl()
|
||||
{
|
||||
return $this->ssl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Adapter;
|
||||
|
||||
use League\Flysystem\AdapterInterface as LeagueAdapterInterface;
|
||||
use League\Flysystem\Config as LeagueConfig;
|
||||
use League\Flysystem\Util;
|
||||
use League\Flysystem\Util\ContentListingFormatter;
|
||||
use Xentral\Components\Filesystem\PathInfo;
|
||||
|
||||
final class LeagueAdapterWrapper implements AdapterInterface
|
||||
{
|
||||
/** @var LeagueAdapterInterface $league */
|
||||
private $league;
|
||||
|
||||
/** @var bool $caseSensitive */
|
||||
private $caseSensitive = true;
|
||||
|
||||
/**
|
||||
* @param LeagueAdapterInterface $league
|
||||
*/
|
||||
public function __construct(LeagueAdapterInterface $league)
|
||||
{
|
||||
$this->league = $league;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return (bool)$this->league->has($path) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return PathInfo|false
|
||||
*/
|
||||
public function getInfo($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$metainfo = $this->league->getMetadata($path);
|
||||
if (!$metainfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$directory = Util::dirname($path);
|
||||
$metainfo['path'] = $path;
|
||||
|
||||
$formatter = new ContentListingFormatter($directory, false, $this->caseSensitive);
|
||||
$contents = $formatter->formatListing([$metainfo]);
|
||||
if (count($contents) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new PathInfo($contents[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function read($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getMetadata($path);
|
||||
if ($meta['type'] === self::TYPE_DIR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$result = $this->league->read($path);
|
||||
if (!$result || !isset($result['contents'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result['contents'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function readStream($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getMetadata($path);
|
||||
if ($meta['type'] === self::TYPE_DIR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->league->readStream($path);
|
||||
if (!$result || !isset($result['stream'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result['stream'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listContents($directory = '', $recursive = false)
|
||||
{
|
||||
$directory = $this->normalizePath($directory);
|
||||
$contents = $this->getLeagueAdapter()->listContents($directory, $recursive);
|
||||
$formatter = new ContentListingFormatter($directory, $recursive, $this->caseSensitive);
|
||||
|
||||
return $formatter->formatListing($contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getMetadata($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->getMetadata($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getType($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getMetadata($path);
|
||||
if ($meta === false || !isset($meta['type'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $meta['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getSize($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getSize($path);
|
||||
if ($meta === false || !isset($meta['size'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$meta['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getTimestamp($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getTimestamp($path);
|
||||
if ($meta === false || !isset($meta['timestamp'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$meta['timestamp'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getMimetype($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$meta = $this->league->getMimetype($path);
|
||||
if ($meta === false || !isset($meta['mimetype'])) {
|
||||
return false;
|
||||
}
|
||||
if ($meta['type'] === 'dir') {
|
||||
return 'directory';
|
||||
}
|
||||
|
||||
return $meta['mimetype'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write($path, $contents, array $config = [])
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->write($path, $contents, new LeagueConfig($config)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file using a stream
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeStream($path, $resource, array $config = [])
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->writeStream($path, $resource, new LeagueConfig($config)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update($path, $contents, array $config = [])
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->update($path, $contents, new LeagueConfig($config)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing file using a stream
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateStream($path, $resource, array $config = [])
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->updateStream($path, $resource, new LeagueConfig($config)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function rename($path, $newpath)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$newpath = $this->normalizePath($newpath);
|
||||
|
||||
return $this->league->rename($path, $newpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file to new location
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function copy($path, $newpath)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$newpath = $this->normalizePath($newpath);
|
||||
|
||||
return $this->league->copy($path, $newpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a single file
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($path)
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->league->delete($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory and its contents
|
||||
*
|
||||
* @param string $directory
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteDir($directory)
|
||||
{
|
||||
$directory = $this->normalizePath($directory);
|
||||
|
||||
return $this->league->deleteDir($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createDir($directory, array $config = [])
|
||||
{
|
||||
$directory = $this->normalizePath($directory);
|
||||
|
||||
return $this->league->createDir($directory, new LeagueConfig($config)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeagueAdapterInterface
|
||||
*/
|
||||
public function getLeagueAdapter()
|
||||
{
|
||||
return $this->league;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function normalizePath($path)
|
||||
{
|
||||
return Util::normalizePath($path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Adapter;
|
||||
|
||||
use Xentral\Components\Filesystem\PathInfo;
|
||||
|
||||
interface ReaderAdapterInterface
|
||||
{
|
||||
const TYPE_DIR = 'dir';
|
||||
const TYPE_FILE = 'file';
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return PathInfo|false
|
||||
*/
|
||||
public function getInfo($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function read($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function readStream($path);
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listContents($directory = '', $recursive = false);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false [dir|file]
|
||||
*/
|
||||
public function getType($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getMetadata($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getSize($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getTimestamp($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getMimetype($path);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'FilesystemFactory' => 'onInitFilesystemFactory',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return FilesystemFactory
|
||||
*/
|
||||
public static function onInitFilesystemFactory(ContainerInterface $container)
|
||||
{
|
||||
return new FilesystemFactory($container->get('Database'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DirNotFoundException extends RuntimeException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileExistsException extends RuntimeException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FileNotFoundException extends RuntimeException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FilesystemException extends RuntimeException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface FilesystemExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class RootViolationException extends LogicException implements FilesystemExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use Xentral\Components\Filesystem\Adapter\AdapterInterface;
|
||||
use Xentral\Components\Filesystem\Exception\DirNotFoundException;
|
||||
use Xentral\Components\Filesystem\Exception\FileExistsException;
|
||||
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Filesystem\Exception\RootViolationException;
|
||||
|
||||
final class Filesystem implements FilesystemInterface
|
||||
{
|
||||
/** @var AdapterInterface $adapter */
|
||||
private $adapter;
|
||||
|
||||
/**
|
||||
* @param AdapterInterface $adapter
|
||||
*/
|
||||
public function __construct(AdapterInterface $adapter)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if file or directory exists
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($path)
|
||||
{
|
||||
return $this->adapter->has($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return PathInfo|false
|
||||
*/
|
||||
public function getInfo($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->getInfo($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory contents
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listContents($directory = '', $recursive = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$contents = $this->adapter->listContents($directory, $recursive);
|
||||
foreach ($contents as $metainfo) {
|
||||
$result[] = PathInfo::fromMeta($metainfo);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists only directories
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listDirs($directory = '', $recursive = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$contents = $this->adapter->listContents($directory, $recursive);
|
||||
foreach ($contents as $metainfo) {
|
||||
if ($metainfo['type'] === 'dir') {
|
||||
$result[] = PathInfo::fromMeta($metainfo);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists only files
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listFiles($directory = '', $recursive = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$contents = $this->adapter->listContents($directory, $recursive);
|
||||
foreach ($contents as $metainfo) {
|
||||
if ($metainfo['type'] === 'file') {
|
||||
$result[] = PathInfo::fromMeta($metainfo);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List only paths as strings
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|string[]
|
||||
*/
|
||||
public function listPaths($directory = '', $recursive = false)
|
||||
{
|
||||
$contents = $this->adapter->listContents($directory, $recursive);
|
||||
|
||||
return array_column($contents, 'path');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string [dir|file]
|
||||
*/
|
||||
public function getType($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->getType($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filesize
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getSize($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->getSize($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp from last update
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getTimestamp($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->getTimestamp($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getMimetype($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->getMimetype($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function read($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->read($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function readStream($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->readStream($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @throws FileExistsException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write($path, $contents, array $config = [])
|
||||
{
|
||||
if ($this->has($path)) {
|
||||
throw new FileExistsException(sprintf('File "%s" exists already.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->write($path, $contents, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws FileExistsException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeStream($path, $resource, array $config = [])
|
||||
{
|
||||
if ($this->has($path)) {
|
||||
throw new FileExistsException(sprintf('File "%s" exists already.', $path));
|
||||
}
|
||||
if (!is_resource($resource)) {
|
||||
throw new InvalidArgumentException('Second parameter must be a resource.');
|
||||
}
|
||||
|
||||
return $this->adapter->writeStream($path, $resource, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file or updates the file contents
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function put($path, $contents, array $config = [])
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
return $this->adapter->write($path, $contents, $config);
|
||||
}
|
||||
|
||||
return $this->adapter->update($path, $contents, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file or updates the file contents
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function putStream($path, $resource, array $config = [])
|
||||
{
|
||||
if (!is_resource($resource)) {
|
||||
throw new InvalidArgumentException('Second parameter must be a resource.');
|
||||
}
|
||||
|
||||
if (!$this->has($path)) {
|
||||
return $this->adapter->writeStream($path, $resource, $config);
|
||||
}
|
||||
|
||||
return $this->adapter->updateStream($path, $resource, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @throws FileExistsException
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function rename($path, $newpath)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
if ($this->has($newpath)) {
|
||||
throw new FileExistsException(sprintf('File "%s" exists already.', $newpath));
|
||||
}
|
||||
|
||||
return $this->adapter->rename($path, $newpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file to new location
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @throws FileExistsException
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function copy($path, $newpath)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
if ($this->has($newpath)) {
|
||||
throw new FileExistsException(sprintf('File "%s" exists already.', $newpath));
|
||||
}
|
||||
|
||||
return $this->adapter->copy($path, $newpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a single file
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($path)
|
||||
{
|
||||
if (!$this->has($path)) {
|
||||
throw new FileNotFoundException(sprintf('File "%s" not found.', $path));
|
||||
}
|
||||
|
||||
return $this->adapter->delete($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory and all its contents
|
||||
*
|
||||
* @param string $dirname
|
||||
*
|
||||
* @throws DirNotFoundException
|
||||
* @throws RootViolationException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteDir($dirname)
|
||||
{
|
||||
$info = $this->adapter->getInfo($dirname);
|
||||
if (!$info) {
|
||||
throw new DirNotFoundException(sprintf('Directory "%s" not found.', $dirname));
|
||||
}
|
||||
if ($info->getPath() === '') {
|
||||
throw new RootViolationException('Root directory can not be deleted.');
|
||||
}
|
||||
|
||||
return $this->adapter->deleteDir($dirname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory
|
||||
*
|
||||
* @param string $dirname
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createDir($dirname, array $config = [])
|
||||
{
|
||||
return $this->adapter->createDir($dirname, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AdapterInterface
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
return $this->adapter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use Exception;
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Components\Filesystem\Adapter\FtpConfig;
|
||||
use Xentral\Components\Filesystem\Adapter\LeagueAdapterWrapper;
|
||||
use Xentral\Components\Filesystem\Exception\FilesystemException;
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Filesystem\Flysystem\FtpAdapterDecorator;
|
||||
use Xentral\Components\Filesystem\Flysystem\LocalAdapterDecorator;
|
||||
|
||||
final class FilesystemFactory
|
||||
{
|
||||
/** @var Database $db */
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* @param Database $database
|
||||
*/
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
$this->db = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $root Absolute path
|
||||
* @param array $config
|
||||
*
|
||||
* @return FilesystemInterface
|
||||
*/
|
||||
public function createLocal($root, array $config = [])
|
||||
{
|
||||
try {
|
||||
$writeFlags = isset($config['write_flags']) ? $config['write_flags'] : LOCK_EX;
|
||||
$linkHandling = isset($config['link_handling']) ? $config['link_handling'] : LocalAdapterDecorator::SKIP_LINKS;
|
||||
$permissions = isset($config['permissions']) ? $config['permissions'] : [];
|
||||
|
||||
$leagueLocalAdapter = new LocalAdapterDecorator($root, $writeFlags, $linkHandling, $permissions);
|
||||
$leagueAdapterWrapper = new LeagueAdapterWrapper($leagueLocalAdapter);
|
||||
|
||||
return new Filesystem($leagueAdapterWrapper);
|
||||
//
|
||||
} catch (Exception $e) {
|
||||
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FtpConfig $config
|
||||
*
|
||||
* @return FilesystemInterface
|
||||
*/
|
||||
public function createFtp(FtpConfig $config)
|
||||
{
|
||||
try {
|
||||
$leagueFtpAdapter = new FtpAdapterDecorator($config->toArray());
|
||||
$leagueAdapterWrapper = new LeagueAdapterWrapper($leagueFtpAdapter);
|
||||
|
||||
return new Filesystem($leagueAdapterWrapper);
|
||||
//
|
||||
} catch (Exception $e) {
|
||||
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FilesystemInterface $filesystem
|
||||
* @param int $syncId
|
||||
*
|
||||
* @return FilesystemSyncCache
|
||||
*/
|
||||
public function createSync(FilesystemInterface $filesystem, $syncId)
|
||||
{
|
||||
try {
|
||||
if (get_class($filesystem) === FilesystemSyncCache::class) {
|
||||
throw new InvalidArgumentException('FilesystemSyncWrapper can not wrap it self.');
|
||||
}
|
||||
|
||||
return new FilesystemSyncCache($this->db, $filesystem, $syncId);
|
||||
//
|
||||
} catch (Exception $e) {
|
||||
throw new FilesystemException($e->getMessage(), (int)$e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use Xentral\Components\Filesystem\Adapter\AdapterInterface;
|
||||
use Xentral\Components\Filesystem\Exception\DirNotFoundException;
|
||||
use Xentral\Components\Filesystem\Exception\FileExistsException;
|
||||
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
use Xentral\Components\Filesystem\Exception\RootViolationException;
|
||||
|
||||
interface FilesystemInterface
|
||||
{
|
||||
/**
|
||||
* Checks if file or directory exists
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return PathInfo|false
|
||||
*/
|
||||
public function getInfo($path);
|
||||
|
||||
/**
|
||||
* List directory contents
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listContents($directory = '', $recursive = false);
|
||||
|
||||
// public function filterContents(array $filter = [], $directory = '', $recursive = false); // @todo
|
||||
//
|
||||
// // @todo ExtendedLocal
|
||||
// public function isReadable();
|
||||
// public function isWriteable();
|
||||
// public function getOwner();
|
||||
// public function getGroup();
|
||||
|
||||
/**
|
||||
* Lists only directories
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listDirs($directory = '', $recursive = false);
|
||||
|
||||
/**
|
||||
* Lists only files
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listFiles($directory = '', $recursive = false);
|
||||
|
||||
/**
|
||||
* List only paths as strings
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|string[]
|
||||
*/
|
||||
public function listPaths($directory = '', $recursive = false);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string [dir|file]
|
||||
*/
|
||||
public function getType($path);
|
||||
|
||||
/**
|
||||
* Gets the filesize
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return int|false
|
||||
*/
|
||||
public function getSize($path);
|
||||
|
||||
/**
|
||||
* Gets the timestamp from last update
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getTimestamp($path);
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getMimetype($path);
|
||||
|
||||
/**
|
||||
* Read file content
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function read($path);
|
||||
|
||||
/**
|
||||
* Read file content
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function readStream($path);
|
||||
|
||||
/**
|
||||
* Writes to a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @throws FileExistsException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write($path, $contents, array $config = []);
|
||||
|
||||
/**
|
||||
* Writes to a new file
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
* @throws FileExistsException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeStream($path, $resource, array $config = []);
|
||||
|
||||
/**
|
||||
* Creates a file or updates the file contents
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function put($path, $contents, array $config = []);
|
||||
|
||||
/**
|
||||
* Creates a file or updates the file contents
|
||||
*
|
||||
* @param string $path
|
||||
* @param resource $resource
|
||||
* @param array $config
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function putStream($path, $resource, array $config = []);
|
||||
|
||||
/**
|
||||
* Renames a file
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @throws FileExistsException
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function rename($path, $newpath);
|
||||
|
||||
/**
|
||||
* Copies a file to new location
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $newpath
|
||||
*
|
||||
* @throws FileExistsException
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function copy($path, $newpath);
|
||||
|
||||
/**
|
||||
* Deletes a single file
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete($path);
|
||||
|
||||
/**
|
||||
* Deletes a directory and all its contents
|
||||
*
|
||||
* @param string $dirname
|
||||
*
|
||||
* @throws DirNotFoundException
|
||||
* @throws RootViolationException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteDir($dirname);
|
||||
|
||||
/**
|
||||
* Creates a directory
|
||||
*
|
||||
* @param string $dirname
|
||||
* @param array $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function createDir($dirname, array $config = []);
|
||||
|
||||
/**
|
||||
* @return AdapterInterface
|
||||
*/
|
||||
public function getAdapter();
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Components\Filesystem\Exception\FileNotFoundException;
|
||||
use Xentral\Components\Filesystem\Exception\FilesystemException;
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @todo Datenbank anlegen
|
||||
*/
|
||||
final class FilesystemSyncCache implements FilesystemInterface
|
||||
{
|
||||
/** @var Database $db */
|
||||
private $db;
|
||||
|
||||
/** @var FilesystemInterface $fs */
|
||||
private $fs;
|
||||
|
||||
/** @var int $syncId */
|
||||
private $syncId;
|
||||
|
||||
/**
|
||||
* @param Database $database
|
||||
* @param FilesystemInterface $filesystem
|
||||
* @param int $syncId
|
||||
*/
|
||||
public function __construct(Database $database, FilesystemInterface $filesystem, $syncId)
|
||||
{
|
||||
if ((int)$syncId <= 0) {
|
||||
throw new InvalidArgumentException(sprintf('Sync-ID "%s" is invalid.', $syncId));
|
||||
}
|
||||
|
||||
$this->db = $database;
|
||||
$this->fs = $filesystem;
|
||||
$this->syncId = (int)$syncId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listChanges($directory = '', $recursive = false)
|
||||
{
|
||||
$result = $this->fs->listContents($directory, $recursive);
|
||||
|
||||
$location = PathUtil::normalizePath($directory);
|
||||
$cache = $this->readCache($location, $recursive);
|
||||
|
||||
foreach ($result as $item) {
|
||||
$path = $item->getPath();
|
||||
|
||||
// Ignore directories; only files are nessesary for syncing
|
||||
if ($item->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Defaults
|
||||
$item->set('missing', false);
|
||||
$item->set('modified', null);
|
||||
|
||||
if (!array_key_exists($path, $cache)) {
|
||||
$item->set('missing', true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int)$cache[$path]['size'] !== (int)$item->getSize()) {
|
||||
$item->set('modified', true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int)$cache[$path]['timestamp'] !== (int)$item->getTimestamp()) {
|
||||
$item->set('modified', true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item->set('modified', false);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists deleted files
|
||||
*
|
||||
* Lists files that are present in cache but does not exist on the filesystem any more.
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array|PathInfo[]
|
||||
*/
|
||||
public function listDeleted($directory = '', $recursive = false)
|
||||
{
|
||||
$paths = $this->fs->listPaths($directory, $recursive);
|
||||
|
||||
$location = PathUtil::normalizePath($directory);
|
||||
$cache = $this->readCache($location, $recursive);
|
||||
|
||||
foreach ($cache as $path => $cacheItem) {
|
||||
if (in_array($path, $paths, true)) {
|
||||
unset($cache[$path]);
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($cache as $cacheItem) {
|
||||
$pathinfo = PathUtil::pathinfo($cacheItem['path']);
|
||||
$result[] = new PathInfo(array_merge($cacheItem, $pathinfo));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function has($path)
|
||||
{
|
||||
return $this->fs->has($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getInfo($path)
|
||||
{
|
||||
return $this->fs->getInfo($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getType($path)
|
||||
{
|
||||
return $this->fs->getType($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getSize($path)
|
||||
{
|
||||
return $this->fs->getSize($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getTimestamp($path)
|
||||
{
|
||||
return $this->fs->getTimestamp($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getMimetype($path)
|
||||
{
|
||||
return $this->fs->getMimetype($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function listContents($directory = '', $recursive = false)
|
||||
{
|
||||
return $this->fs->listContents($directory, $recursive);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function listDirs($directory = '', $recursive = false)
|
||||
{
|
||||
return $this->fs->listDirs($directory, $recursive);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function listFiles($directory = '', $recursive = false)
|
||||
{
|
||||
return $this->fs->listFiles($directory, $recursive);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function listPaths($directory = '', $recursive = false)
|
||||
{
|
||||
return $this->fs->listPaths($directory, $recursive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function read($path)
|
||||
{
|
||||
$result = $this->fs->read($path);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function readStream($path)
|
||||
{
|
||||
$result = $this->fs->readStream($path);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function write($path, $contents, array $config = [])
|
||||
{
|
||||
$result = $this->fs->write($path, $contents, $config);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function writeStream($path, $resource, array $config = [])
|
||||
{
|
||||
$result = $this->fs->writeStream($path, $resource, $config);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function put($path, $contents, array $config = [])
|
||||
{
|
||||
$result = $this->fs->put($path, $contents, $config);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function putStream($path, $resource, array $config = [])
|
||||
{
|
||||
$result = $this->fs->putStream($path, $resource, $config);
|
||||
$this->updateCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function delete($path)
|
||||
{
|
||||
$result = $this->fs->delete($path);
|
||||
$this->deleteCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a single file, but without Exception if path does not exist.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function softDelete($path)
|
||||
{
|
||||
$result = false;
|
||||
try {
|
||||
$this->deleteCachePath($path);
|
||||
$result = $this->fs->delete($path);
|
||||
} catch (FileNotFoundException $e) {
|
||||
// nope - its soft
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function deleteDir($dirname)
|
||||
{
|
||||
$result = $this->fs->deleteDir($dirname);
|
||||
$this->deleteCacheDir($dirname);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createDir($dirname, array $config = [])
|
||||
{
|
||||
return $this->fs->createDir($dirname, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rename($path, $newpath)
|
||||
{
|
||||
$result = $this->fs->rename($path, $newpath);
|
||||
$this->deleteCachePath($path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function copy($path, $newpath)
|
||||
{
|
||||
$result = $this->fs->copy($path, $newpath);
|
||||
$this->updateCachePath($newpath);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
return $this->fs->getAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function readCache($path, $recursive = false)
|
||||
{
|
||||
$this->ensureDependencies();
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->db->fetchAssoc(
|
||||
'SELECT f.path, f.dirname, f.type, f.size, f.updated_at AS timestamp ' .
|
||||
'FROM sync_files AS f ' .
|
||||
'WHERE f.sync_id = :sync_id AND f.dirname LIKE :path_prefix',
|
||||
[
|
||||
'sync_id' => $this->syncId,
|
||||
'path_prefix' => $recursive === true ? $path . '%' : $path,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function updateCachePath($path)
|
||||
{
|
||||
$this->ensureDependencies();
|
||||
$info = $this->fs->getInfo($path);
|
||||
|
||||
$this->db->perform(
|
||||
'REPLACE INTO sync_files (sync_id, `path`, dirname, type, size, updated_at) ' .
|
||||
'VALUES (:sync_id, :path, :dirname, :type, :size, :updated_at)',
|
||||
[
|
||||
'sync_id' => $this->syncId,
|
||||
'path' => $info->getPath(),
|
||||
'dirname' => $info->getDir(),
|
||||
'type' => $info->getType(),
|
||||
'size' => (int)$info->getSize(),
|
||||
'updated_at' => (int)$info->getTimestamp(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return int Deleted row count
|
||||
*/
|
||||
private function deleteCachePath($path)
|
||||
{
|
||||
$this->ensureDependencies();
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
return $this->db->fetchAffected(
|
||||
'DELETE FROM sync_files WHERE sync_id = :sync_id AND `path` = :path',
|
||||
['sync_id' => $this->syncId, 'path' => $path]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dirname
|
||||
*
|
||||
* @return int Deleted row count
|
||||
*/
|
||||
private function deleteCacheDir($dirname)
|
||||
{
|
||||
$this->ensureDependencies();
|
||||
$dirname = $this->normalizePath($dirname);
|
||||
|
||||
return $this->db->fetchAffected(
|
||||
'DELETE FROM sync_files WHERE sync_id = :sync_id AND `dirname` LIKE :dirname',
|
||||
['sync_id' => $this->syncId, 'dirname' => $dirname . '%']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function normalizePath($path)
|
||||
{
|
||||
return PathUtil::normalizePath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FilesystemException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function ensureDependencies()
|
||||
{
|
||||
if ($this->db === null || $this->syncId === null) {
|
||||
throw new FilesystemException('Can not continue. Required dependencies are missing.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Flysystem;
|
||||
|
||||
use League\Flysystem\Adapter\Ftp;
|
||||
|
||||
final class FtpAdapterDecorator extends Ftp
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getMetadata($path)
|
||||
{
|
||||
$metadata = parent::getMetadata($path);
|
||||
if ($metadata === false) {
|
||||
return false; // File does not exist
|
||||
}
|
||||
|
||||
if ($metadata['type'] === 'dir') {
|
||||
$metadata['timestamp'] = null; // ftp_mdtm() does not work with directories.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ($metadata['timestamp'] === null) {
|
||||
$data = $this->getTimestamp($path);
|
||||
$metadata['timestamp'] = $data !== false && isset($data['timestamp']) ? $data['timestamp'] : null;
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function normalizeListing(array $listing, $prefix = '')
|
||||
{
|
||||
$result = parent::normalizeListing($listing, $prefix);
|
||||
|
||||
foreach ($result as &$item) {
|
||||
if ($item['type'] === 'dir') {
|
||||
$item['timestamp'] = null; // ftp_mdtm() does not work with directories.
|
||||
continue;
|
||||
}
|
||||
if (!isset($item['timestamp'])) {
|
||||
$data = $this->getTimestamp($item['path']);
|
||||
$item['timestamp'] = $data !== false && isset($data['timestamp']) ? $data['timestamp'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Flysystem;
|
||||
|
||||
use League\Flysystem\Adapter\Local;
|
||||
|
||||
final class LocalAdapterDecorator extends Local
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use League\Flysystem\Util;
|
||||
use Xentral\Components\Filesystem\Exception\InvalidArgumentException;
|
||||
|
||||
final class PathInfo
|
||||
{
|
||||
const TYPE_FILE = 'file';
|
||||
const TYPE_DIR = 'dir';
|
||||
|
||||
/** @var array $readonlyProperties */
|
||||
private static $readonlyProperties = [
|
||||
'type',
|
||||
'path',
|
||||
'dirname',
|
||||
'filename',
|
||||
'basename',
|
||||
'extension',
|
||||
'timestamp',
|
||||
'size',
|
||||
];
|
||||
|
||||
/** @var array $data */
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
// required
|
||||
$data['type'] = (string)$data['type'];
|
||||
$data['path'] = (string)$data['path'];
|
||||
$data['dirname'] = (string)$data['dirname'];
|
||||
$data['filename'] = (string)$data['filename'];
|
||||
$data['basename'] = (string)$data['basename'];
|
||||
|
||||
// optional
|
||||
$data['extension'] = !empty($data['extension']) ? (string)$data['extension'] : null;
|
||||
$data['timestamp'] = is_numeric($data['timestamp']) ? (int)$data['timestamp'] : null;
|
||||
$data['size'] = is_numeric($data['size']) ? (int)$data['size'] : null;
|
||||
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $metainfo
|
||||
*
|
||||
* @return PathInfo
|
||||
*/
|
||||
public static function fromMeta(array $metainfo = [])
|
||||
{
|
||||
$pathinfo = Util::pathinfo($metainfo['path']);
|
||||
|
||||
return new PathInfo(array_merge($pathinfo, $metainfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFile()
|
||||
{
|
||||
return $this->getType() === self::TYPE_FILE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDir()
|
||||
{
|
||||
return $this->getType() === self::TYPE_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string [dir|file]
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->data['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Filename with extension and without path
|
||||
*/
|
||||
public function getBasename()
|
||||
{
|
||||
return $this->data['basename'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Filename without extension and without path
|
||||
*/
|
||||
public function getFilename()
|
||||
{
|
||||
return $this->data['filename'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null File extension or null if directory
|
||||
*/
|
||||
public function getExtension()
|
||||
{
|
||||
return $this->data['extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Path without filename
|
||||
*/
|
||||
public function getDir()
|
||||
{
|
||||
return $this->data['dirname'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relativer Pfad zum Mountpoint
|
||||
*
|
||||
* @return string Path with filename
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->data['path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null File size or null if not available
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->data['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null Last updated timestamp or null if not available
|
||||
*/
|
||||
public function getTimestamp()
|
||||
{
|
||||
return $this->data['timestamp'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return isset($this->data[(string)$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($name)
|
||||
{
|
||||
if ($this->has($name)) {
|
||||
return $this->data[(string)$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($name, $value)
|
||||
{
|
||||
if (in_array((string)$name, self::$readonlyProperties, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Property "%s" is readonly.', $name));
|
||||
}
|
||||
|
||||
$this->data[(string)$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name)
|
||||
{
|
||||
return $this->has($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->set($name, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem;
|
||||
|
||||
use League\Flysystem\Util;
|
||||
|
||||
final class PathUtil extends Util
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Plugin;
|
||||
|
||||
use Xentral\Components\Filesystem\FilesystemInterface;
|
||||
|
||||
final class FilterPathsPlugin implements PluginInterface
|
||||
{
|
||||
/** @var FilesystemInterface $filesystem */
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return 'filterPaths';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FilesystemInterface $filesystem
|
||||
*/
|
||||
public function setFilesystem(FilesystemInterface $filesystem)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example filterPaths(['extension' => 'php', 'filename' => 'Bootstrap'])
|
||||
*
|
||||
* @param array $filter
|
||||
* @param string $path
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function handle(array $filter = [], $path = '', $recursive = false)
|
||||
{
|
||||
$result = [];
|
||||
$contents = $this->filesystem->listContents($path, $recursive);
|
||||
|
||||
foreach ($contents as $object) {
|
||||
$matchesFilter = true;
|
||||
|
||||
foreach ($filter as $property => $value) {
|
||||
if (!isset($object[$property])) {
|
||||
$matchesFilter = false;
|
||||
continue;
|
||||
}
|
||||
if ($object[$property] !== $value) {
|
||||
$matchesFilter = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchesFilter === true) {
|
||||
$result[] = $object['path'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Plugin;
|
||||
|
||||
use League\Flysystem\Plugin\AbstractPlugin;
|
||||
|
||||
final class ListDirectoriesPlugin extends AbstractPlugin
|
||||
{
|
||||
/**
|
||||
* Get the method name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return 'listDirectories';
|
||||
}
|
||||
|
||||
/**
|
||||
* List all directories in the directory.
|
||||
*
|
||||
* @param string $directory
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function handle($directory = '', $recursive = false)
|
||||
{
|
||||
$contents = $this->filesystem->listContents($directory, $recursive);
|
||||
|
||||
$filter = function ($object) {
|
||||
return $object['type'] === 'dir';
|
||||
};
|
||||
|
||||
return array_values(array_filter($contents, $filter));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Filesystem\Plugin;
|
||||
|
||||
use Xentral\Components\Filesystem\FilesystemInterface;
|
||||
|
||||
interface PluginInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod();
|
||||
|
||||
/**
|
||||
* @param FilesystemInterface $filesystem
|
||||
*/
|
||||
public function setFilesystem(FilesystemInterface $filesystem);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
# Filesystem-Komponente
|
||||
|
||||
## Beispiele
|
||||
|
||||
### FTP
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @var \Xentral\Components\Filesystem\FilesystemFactory $factory */
|
||||
$factory = $this->app->Container->get('FilesystemFactory');
|
||||
|
||||
$config = new \Xentral\Components\Filesystem\Adapter\FtpConfig('192.168.0.123', 'username', 'passwort', '/root-dir');
|
||||
$ftp = $factory->createFtp($config);
|
||||
|
||||
if ($ftp->has('/some/file.txt')) {
|
||||
$contents = $ftp->read('/some/file.txt');
|
||||
} else {
|
||||
$ftp->write('/some/file.txt', 'hello world!');
|
||||
}
|
||||
```
|
||||
|
||||
### Lokales Dateisystem
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @var \Xentral\Components\Filesystem\FilesystemFactory $factory */
|
||||
$factory = $this->app->Container->get('FilesystemFactory');
|
||||
|
||||
$local = $factory->createLocal(dirname(__DIR__));
|
||||
|
||||
if ($local->has('/some/file.txt')) {
|
||||
$contents = $local->read('/some/file.txt');
|
||||
} else {
|
||||
$local->write('/some/file.txt', 'hello world!');
|
||||
}
|
||||
```
|
||||
|
||||
### Große Dateien kopieren
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$ftp = $factory->createFtp($config);
|
||||
$local = $factory->createLocal(dirname(__DIR__));
|
||||
|
||||
$ftp->writeStream('/ziel-pfad', $local->readStream('/quell-pfad'));
|
||||
```
|
||||
|
||||
### Datei-Uploads
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$factory = $this->app->Container->get('FilesystemFactory');
|
||||
$local = $factory->createLocal(dirname(__DIR__));
|
||||
|
||||
$stream = fopen($_FILES['upload']['tmp_name'], 'rb+');
|
||||
$local->writeStream(
|
||||
'uploads/' . $_FILES['upload']['name'],
|
||||
$stream
|
||||
);
|
||||
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
```
|
||||
|
||||
### FilesystemSyncCache
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$ftp = $factory->createFtp($config);
|
||||
$sync = $factory->createSync($ftp, 1);
|
||||
|
||||
$changes = $sync->listChanges('/sub', true); // Hinzugekommene und geänderte Dateien abrufen
|
||||
$deletes = $sync->listDeleted('/sub', true); // Gelöschte Dateien abrufen
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### FilesystemInterface
|
||||
|
||||
---
|
||||
|
||||
#### `has($path)`
|
||||
|
||||
Prüfen ob eine Datei oder ein Verzeichnis existiert.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `listContents($directory = '', $recursive = false)`
|
||||
|
||||
Verzeichnisinhalte abrufen.
|
||||
|
||||
Rückgabe: `array` mit `PathInfo`-Objekten
|
||||
|
||||
---
|
||||
|
||||
#### `listDirs($directory = '', $recursive = false)`
|
||||
|
||||
Verzeichnisinhalte abrufen; nur Verzeichnisse.
|
||||
|
||||
Rückgabe: `array` mit `PathInfo`-Objekten
|
||||
|
||||
---
|
||||
|
||||
#### `listFiles($directory = '', $recursive = false)`
|
||||
|
||||
Verzeichnisinhalte abrufen; nur Dateien.
|
||||
|
||||
Rückgabe: `array` mit `PathInfo`-Objekten
|
||||
|
||||
---
|
||||
|
||||
#### `listPaths($directory = '', $recursive = false)`
|
||||
|
||||
Verzeichnisinhalte abrufen; nur Pfadauflistung; keine Details.
|
||||
|
||||
Rückgabe: `array` mit Pfaden als `string`
|
||||
|
||||
---
|
||||
|
||||
#### `getInfo($path)`
|
||||
|
||||
Informationen über eine Datei oder ein Verzeichnis abrufen.
|
||||
|
||||
Rückgabe: `PathInfo`-Objekt
|
||||
|
||||
---
|
||||
|
||||
#### `getType($path)`
|
||||
|
||||
Prüfen ob Pfad eine Datei oder ein Verzeichnis ist.
|
||||
|
||||
Rückgabe: `'dir'` oder `'file''`
|
||||
|
||||
---
|
||||
|
||||
#### `getSize($path)`
|
||||
|
||||
Dateigröße abrufen.
|
||||
|
||||
Rückgabe: Dateigröße als `int` oder `false` bei einem Verzeichnis.
|
||||
|
||||
---
|
||||
|
||||
#### `getTimestamp($path)`
|
||||
|
||||
Datum der letzten Änderung abrufen.
|
||||
|
||||
Rückgabe: Timestamp als `int` oder `false` falls Information nicht verfügbar
|
||||
(z.B. bei Verzeichnissen über FTP).
|
||||
|
||||
---
|
||||
|
||||
#### `getMimetype($path)`
|
||||
|
||||
Mimetype abrufen.
|
||||
|
||||
Rückgabe: Mimetype als `string` oder `'directory'` bei einem Verzeichnis.
|
||||
|
||||
---
|
||||
|
||||
#### `read($path)`
|
||||
|
||||
Datei-Inhalt abrufen.
|
||||
|
||||
Rückgabe: `string`
|
||||
|
||||
---
|
||||
|
||||
#### `readStream($path)`
|
||||
|
||||
Datei-Inhalt als Stream abrufen.
|
||||
|
||||
Rückgabe: `resource`
|
||||
|
||||
---
|
||||
|
||||
#### `write($path, $contents, array $config = [])`
|
||||
#### `writeStream($path, $resource, array $config = [])`
|
||||
|
||||
Datei anlegen.
|
||||
|
||||
Besonderheit: Wenn Zieldatei bereits existiert, wird eine `FileExistsException` geworfen.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `put($path, $contents, array $config = [])`
|
||||
#### `putStream($path, $resource, array $config = [])`
|
||||
|
||||
Datei anlegen oder überschreiben.
|
||||
|
||||
Besonderheit: Im Unterschied zu `write()` und `writeStream()` wird keine Exception geworfen wenn das Ziel bereits existiert.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `createDir($dirname)`
|
||||
|
||||
Verzeichnis anlegen; funktioniert auch rekursiv.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `deleteDir($dirname)`
|
||||
|
||||
Verzeichnis löschen; funktioniert auch wenn Verzeichnis nicht leer.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `delete($path)`
|
||||
|
||||
Einzelne Datei löschen.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `rename($path, $newpath)`
|
||||
|
||||
Datei umbenennen.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
|
||||
#### `copy($path, $newpath)`
|
||||
|
||||
Datei kopieren.
|
||||
|
||||
Rückgabe: `true` oder `false`
|
||||
|
||||
---
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http;
|
||||
|
||||
use Xentral\Components\Http\Session\SessionHandler;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'Request' => 'onInitRequest',
|
||||
'Session' => 'onInitSession',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public static function onInitRequest()
|
||||
{
|
||||
return Request::createFromGlobals();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Session\Session
|
||||
*/
|
||||
public static function onInitSession()
|
||||
{
|
||||
return SessionHandler::createSession();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
use ArrayIterator;
|
||||
use Iterator;
|
||||
use Xentral\Components\Http\File\FileUpload;
|
||||
|
||||
class FilesCollection implements Iterator
|
||||
{
|
||||
/** @var ArrayIterator $iterator */
|
||||
protected $iterator;
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
*/
|
||||
public function __construct(array $files = [])
|
||||
{
|
||||
$files = $this->loadFilesArray($files);
|
||||
$this->iterator = new ArrayIterator($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all file uploads.
|
||||
*
|
||||
* @return FileUpload[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->iterator->getArrayCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a file upload entry with this name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return $this->iterator->offsetExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an file upload entry.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return FileUpload|mixed
|
||||
*/
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
return $this->has($name) ? $this->iterator->offsetGet($name) : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all upload files are valid.
|
||||
*
|
||||
* @return bool true=all upload files are valid
|
||||
*/
|
||||
public function allValid()
|
||||
{
|
||||
foreach ($this->all() as $name => $upload) {
|
||||
if (!$upload->isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least one file upload has an error.
|
||||
*
|
||||
* @return bool true=there is at least one error
|
||||
*/
|
||||
public function hasErrors()
|
||||
{
|
||||
foreach ($this->all() as $name => $upload) {
|
||||
if ($upload->hasError()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @return mixed FileUpload Object, null on failure
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->iterator->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forward to next element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->iterator->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key of the current element
|
||||
*
|
||||
* @return mixed on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->iterator->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid
|
||||
*
|
||||
* @return boolean true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->iterator->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the Iterator to the first element
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->iterator->rewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fileEntries
|
||||
*
|
||||
* @return bool true=is alternate Array
|
||||
*/
|
||||
protected function isAlternateArray($fileEntries)
|
||||
{
|
||||
if (
|
||||
!isset($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
|
||||
|| is_array($fileEntries[array_keys($fileEntries)[0]]['tmp_name'])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fileEntries
|
||||
*
|
||||
* @return FileUpload[] $files
|
||||
*/
|
||||
protected function loadFilesArray($fileEntries)
|
||||
{
|
||||
if (empty($fileEntries)) {
|
||||
return [];
|
||||
}
|
||||
if ($this->isAlternateArray($fileEntries)) {
|
||||
return $this->convertFilesArray($fileEntries);
|
||||
}
|
||||
|
||||
$files = [];
|
||||
foreach ($fileEntries as $name => $upload) {
|
||||
if (empty($upload['tmp_name'])) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($upload['tmp_name'])) {
|
||||
$files[$name] = FileUpload::fromFilesArray($upload);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds the array with FileUpload instances from alternative array format.
|
||||
*
|
||||
* tested up to 3rd level nesting
|
||||
*
|
||||
* @param array $files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertFilesArray(array $files)
|
||||
{
|
||||
$totalUploads = [];
|
||||
foreach ($files as $name => $file) {
|
||||
$keys = array_keys($file);
|
||||
if (count(array_intersect($keys, ['name', 'tmp_name'])) === 2) {
|
||||
$uploads = [];
|
||||
foreach ($file['tmp_name'] as $k => $v) {
|
||||
$upload = [
|
||||
'tmp_name' => array_key_exists('tmp_name', $file) ? $file['tmp_name'][$k] : null,
|
||||
'name' => array_key_exists('name', $file) ? $file['name'][$k] : null,
|
||||
'type' => array_key_exists('type', $file) ? $file['type'][$k] : null,
|
||||
'size' => array_key_exists('size', $file) ? $file['size'][$k] : null,
|
||||
'error' => array_key_exists('error', $file) ? $file['error'][$k] : null,
|
||||
];
|
||||
if (!empty($upload['tmp_name'])) {
|
||||
$uploads[$k] = FileUpload::fromFilesArray($upload);
|
||||
}
|
||||
}
|
||||
$totalUploads[$name] = $uploads;
|
||||
} else {
|
||||
if (!(isset($file['tmp_name']) && $file['tmp_name'] === '')) {
|
||||
$totalUploads[$name] = $this->convertFilesArray($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $totalUploads;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
class ParameterCollection extends ReadonlyParameterCollection
|
||||
{
|
||||
/**
|
||||
* Sets a parameter; Existing parameter value will be overwritten
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set($name, $value)
|
||||
{
|
||||
$this->params[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets multiple parameters; Existing parameters will be overwritten
|
||||
*
|
||||
* @param array $values
|
||||
*/
|
||||
public function add(array $values)
|
||||
{
|
||||
$this->params = array_merge($this->params, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a parameter
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
unset($this->params[$name]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Http\Collection;
|
||||
|
||||
class ReadonlyParameterCollection
|
||||
{
|
||||
/** @var array $params */
|
||||
protected $params;
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->params = [];
|
||||
foreach ($params as $name => $value) {
|
||||
$this->params[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all parameters and values as associative array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if parameter is available
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return array_key_exists($name, $this->params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
return isset($this->params[$name]) ? $this->params[$name] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value casted to boolean
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $default
|
||||
*
|
||||
* @return bool Returns true for "1", "true", "on" and "yes"; otherwise false
|
||||
*/
|
||||
public function getBool($name, $default = false)
|
||||
{
|
||||
return filter_var($this->get($name, $default), FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameter value casted to integer
|
||||
*
|
||||
* @param string $name
|
||||
* @param int $default
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInt($name, $default = 0)
|
||||
{
|
||||
return (int)$this->get($name, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with digits only (0-9)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDigits($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^0-9]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphabetic characters only (A-Z and a-z)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlpha($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphanumeric characters only (A-Z, a-z and 0-9)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlphaNum($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z0-9]#', '', $this->get($name, $default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with alphanumeric characters and dashes only (A-Z, a-z, 0-9, Minus and Underscore)
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $default
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlphaNumWithDashes($name, $default = '')
|
||||
{
|
||||
return (string)preg_replace('#[^A-Za-z0-9_-]#', '', $this->get($name, $default));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user