Commit 2348eb48 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

RunSQLService : nouveau service permettant d'exécuter des scripts SQL dans une base de données

parent 9bdff60f
Pipeline #4795 passed with stage
in 25 seconds
<?php
namespace UnicaenApp;
use UnicaenApp\Mouchard\MouchardService;
use UnicaenApp\Mouchard\MouchardServiceFactory;
use Locale;
use UnicaenApp\Mvc\Listener\MaintenanceListener;
use UnicaenApp\Mvc\Listener\ModalListener;
use UnicaenApp\Mvc\View\Http\ExceptionStrategy;
use UnicaenApp\Options\ModuleOptions;
use Zend\Console\Adapter\AdapterInterface as Console;
use Zend\Console\Request as ConsoleRequest;
use Zend\EventManager\EventInterface;
use Zend\ServiceManager\ServiceManager;
use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\ControllerPluginProviderInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
use Zend\Console\Request as ConsoleRequest;
use Zend\Validator\AbstractValidator;
use Zend\Mvc\I18n\Translator;
use Locale;
use UnicaenApp\Mvc\View\Http\ExceptionStrategy;
use UnicaenApp\Mvc\Listener\ModalListener;
use Zend\Mvc\View\Console\ExceptionStrategy as ConsoleExceptionStrategy;
use Zend\Mvc\View\Http\ExceptionStrategy as HttpExceptionStrategy;
use Zend\ServiceManager\ServiceManager;
use Zend\Validator\AbstractValidator;
use Zend\View\Helper\Navigation;
use Zend\View\HelperPluginManager;
use Zend\Mvc\View\Http\ExceptionStrategy as HttpExceptionStrategy;
use Zend\Mvc\View\Console\ExceptionStrategy as ConsoleExceptionStrategy;
define('__VENDOR_DIR__', dirname(dirname(__DIR__)));
......@@ -216,4 +215,14 @@ class Module implements
return [];
}
public function getConsoleUsage(Console $console)
{
return [
// command
'run-sql --path= [--connection=]' => "Exécuter un script SQL ou tous les scripts SQL présents dans un répertoire",
// parameters
['--path', "Requis. Chemin vers le script SQL ou le répertoire contenant les scripts à exécuter"],
['--connection', "Facultatif. Identifiant de la connexion Doctrine. Par défaut 'orm_default'"],
];
}
}
......@@ -2,8 +2,13 @@
namespace UnicaenApp;
use UnicaenApp\Controller\ConsoleController;
use UnicaenApp\Controller\ConsoleControllerFactory;
use UnicaenApp\Service\Mailer\MailerService;
use UnicaenApp\Service\Mailer\MailerServiceFactory;
use UnicaenApp\Service\SQL\RunSQLService;
use UnicaenApp\Service\SQL\RunSQLServiceFactory;
use Zend\Mvc\Router\Console\Simple;
return [
'asset_manager' => [
......@@ -211,6 +216,26 @@ return [
],
],
],
'console' => [
'router' => [
'routes' => [
'run-sql' => [
'type' => Simple::class,
'options' => [
'route' => 'run-sql --path= [--connection=]',
'defaults' => [
'controller' => ConsoleController::class,
'action' => 'run-sql',
],
],
],
],
],
'view_manager' => [
'display_not_found_reason' => true,
'display_exceptions' => true,
]
],
'service_manager' => [
'factories' => [
'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
......@@ -257,6 +282,8 @@ return [
'MouchardCompleterMvc' => 'UnicaenApp\Mouchard\MouchardCompleterMvcFactory',
MailerService::class => MailerServiceFactory::class,
RunSQLService::class => RunSQLServiceFactory::class,
],
'shared' => [
'MouchardListenerErrorHandler' => false,
......@@ -363,6 +390,9 @@ return [
'UnicaenApp\Controller\Cache' => 'UnicaenApp\Controller\CacheController',
'UnicaenApp\Controller\Instadia' => 'UnicaenApp\Controller\InstadiaController',
],
'factories' => [
ConsoleController::class => ConsoleControllerFactory::class,
],
'initializers' => [
'UnicaenApp\Service\EntityManagerAwareInitializer',
],
......
<?php
namespace UnicaenApp\Controller;
use Doctrine\DBAL\Connection;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Service\SQL\RunSQLServiceAwareTrait;
use Zend\Log\Filter\Priority;
use Zend\Log\Formatter\Simple;
use Zend\Log\Logger;
use Zend\Log\LoggerAwareTrait;
use Zend\Log\Writer\Stream;
use Zend\Mvc\Controller\AbstractConsoleController;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
class ConsoleController extends AbstractConsoleController
{
use ServiceLocatorAwareTrait;
use LoggerAwareTrait;
use RunSQLServiceAwareTrait;
/**
* @throws \Exception
*/
public function runSqlAction()
{
$path = $this->params('path');
$connection = $this->params('connection', 'orm_default');
$serviceName = "doctrine.connection.$connection";
$logger = $this->createLogger();
$this->runSQLService->setLogger($logger);
$logger->info("### Exécution de scripts SQL ###");
$logger->info(date_format(date_create(), 'd/m/Y H:i:s'));
if (! $this->serviceLocator->has($serviceName)) {
throw new RuntimeException("Connection Doctrine introuvable : $serviceName");
}
/** @var Connection $conn */
$conn = $this->serviceLocator->get($serviceName);
$logger->info("Utilisation de la connexion '$serviceName'.");
$result = $this->runSQLService->runSQL($path, $conn);
if ($exception = $result->getException()) {
$logger->info("Une erreur a été rencontrée!");
$logger->info($exception->getMessage());
if ($previous = $exception->getPrevious()) {
$logger->info($previous->getMessage());
}
$logger->info($exception->getTraceAsString());
} else {
$logger->info("Exécution réalisée avec succès.");
}
$startDate = $result->getStartDate();
$endDate = $result->getEndDate();
$diffDate = date_diff($startDate, $endDate);
$logger->info($diffDate->format('%i min %s sec.'));
}
/**
* @return Logger
*/
private function createLogger()
{
$filter = new Priority(Logger::INFO);
$format = '%message%'; // '%timestamp% %priorityName% (%priority%): %message%' . PHP_EOL;
$formatter = new Simple($format);
$writer = new Stream('php://output');
$writer->addFilter($filter);
$writer->setFormatter($formatter);
/** @var Logger $logger */
$logger = new Logger();
$logger->addWriter($writer);
return $logger;
}
}
<?php
namespace UnicaenApp\Controller;
use UnicaenApp\Service\SQL\RunSQLService;
use Zend\Log\Logger;
use Zend\Log\LoggerInterface;
use Zend\Log\Writer\Noop;
use Zend\Mvc\Controller\ControllerManager;
class ConsoleControllerFactory
{
public function __invoke(ControllerManager $sl)
{
/** @var \UnicaenApp\Service\SQL\RunSQLService $runSQLService */
$runSQLService = $sl->getServiceLocator()->get(RunSQLService::class);
$controller = new ConsoleController();
$controller->setLogger($this->createLogger());
$controller->setServiceLocator($sl->getServiceLocator());
$controller->setRunSQLService($runSQLService);
return $controller;
}
/**
* @return LoggerInterface
*/
private function createLogger()
{
/** @var Logger $logger */
$logger = new Logger();
$logger->addWriter(new Noop());
return $logger;
}
}
\ No newline at end of file
<?php
namespace UnicaenApp\Process;
use DateTime;
use Exception;
use UnicaenApp\Exception\LogicException;
use Zend\Log\Formatter\Simple;
use Zend\Log\Logger;
use Zend\Log\LoggerInterface;
use Zend\Log\Writer\Stream;
use Zend\Log\Writer\WriterInterface;
/**
* Classe permettant de représenter le résultat de l'exécution d'un processus inconnu quelconque, çàd :
* - un témoin de réussite ou non
* - des logs (via un 'log writer' ajouté à un logger externe)
* - une exception éventuelle en cas d'erreur.
* - une date de début d'exécution
* - une date de fin d'exécution
*
* @author Unicaen
*/
abstract class ProcessResult
{
/**
* @var resource
*/
private $logStream;
/**
* @var WriterInterface
*/
private $logWriter;
/**
* @var bool
*/
private $success;
/**
* @var Exception
*/
private $exception;
/**
* @var DateTime
*/
private $startDate;
/**
* @var DateTime
*/
private $endDate;
/**
* RunSqlResult constructor.
*/
public function __construct()
{
$format = '%message%'; // '%timestamp% %priorityName% (%priority%): %message%' . PHP_EOL;
$formatter = new Simple($format);
$this->logStream = fopen('php://memory','r+');
$this->logWriter = new Stream($this->logStream);
$this->logWriter->setFormatter($formatter);
$this->setStartDate(date_create('now'));
}
public function __destruct()
{
fclose($this->logStream);
}
/**
* Attache un logger pour stocker les éventuels logs qu'il génèrera.
*
* Concrètement, cela ajoute au logger spécifié 'writer' permettant qui stockera les logs
* pour les restituer ultèrieurement via la méthode {@see getLog()}.
*
* @param LoggerInterface $logger
* @return self
*/
public function attachLogger(LoggerInterface $logger)
{
if (! $logger instanceof Logger) {
throw new LogicException("Logger spécifié non supporté, désolé!");
}
$logger->addWriter($this->logWriter);
return $this;
}
/**
* Retourne les logs stockés.
*
* @return string
*/
public function getLog()
{
$offset = ftell($this->logStream);
rewind($this->logStream);
$logs = stream_get_contents($this->logStream);
fseek($this->logStream, $offset);
return $logs;
}
/**
* Retourne le booléen indiquant si l'exécution est couronnée de succès ou non.
*
* @return bool
*/
public function isSuccess()
{
return $this->success;
}
/**
* Positionne le booléen indiquant si l'exécution est couronnée de succès ou non.
*
* @param bool $success
* @return self
*/
public function setIsSuccess($success = true)
{
$this->success = (bool) $success;
return $this;
}
/**
* @return DateTime
*/
public function getStartDate(): DateTime
{
return $this->startDate;
}
/**
* @param DateTime $startDate
*/
public function setStartDate(DateTime $startDate = null)
{
$this->startDate = $startDate ?: date_create();
}
/**
* @return DateTime
*/
public function getEndDate(): DateTime
{
return $this->endDate;
}
/**
* @param DateTime $endDate
*/
public function setEndDate(DateTime $endDate = null)
{
$this->endDate = $endDate ?: date_create();
}
/**
* Retourne l'éventuelle exception rencontrée lors de l'exécution.
*
* @return Exception
*/
public function getException()
{
return $this->exception;
}
/**
* Renseigne l'exception rencontrée lors de l'exécution.
*
* @param Exception $exception
* @return self
*/
public function setException(Exception $exception)
{
$this->exception = $exception;
return $this;
}
}
<?php
namespace UnicaenApp\Service\SQL;
use UnicaenApp\Process\ProcessResult;
/**
* Résultat de l'exécution d'instructions SQL.
*
* @author Unicaen
*/
class RunSQLResult extends ProcessResult
{
}
\ No newline at end of file
<?php
namespace UnicaenApp\Service\SQL;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use UnicaenApp\Exception\RuntimeException;
use Zend\Log\LoggerAwareTrait;
use Zend\Stdlib\Glob;
/**
* Service permettant d'exécuter des scripts SQL dans une base de données.
*
* @author Unicaen
*/
class RunSQLService
{
use LoggerAwareTrait;
/**
* Exécute le ou les scripts spécifiés par leurs chemins ou par le chemin de leur répertoire.
*
* NB: chaque script est exécuté dans une nouvelle transaction.
* NB: dans un script, les requêtes sont exécutées une par une.
*
* Le formattage des requêtes SQL dans chaque script n'est pas anodin !
* Exemple de script SQL respectant le formattage requis :
*
```sql
---------------------------------
insert into API_LOG(ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_ETABLISSEMENT, REQ_TABLE)
select API_LOG_ID_SEQ.nextval, 'xxxxx', sysdate, sysdate, 'test', 'hello!!', 'UCN', 'TEST' from dual
/
declare
d date;
begin
select sysdate into d from dual;
insert into API_LOG(ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_ETABLISSEMENT, REQ_TABLE)
select API_LOG_ID_SEQ.nextval, 'yyyyy', d, d, 'test', 'hello!!', 'UCN', 'TEST' from dual;
end;
/
create index TEST_index on FICHIER (HISTO_MODIFICATION ASC)
/
drop index TEST_index
/
---------------------------------
```
*
* @param string $path Chemin absolu d'un script SQL ou d'un répertoire contenant des scripts *.[sS][qQ][lL]
* @param Connection $conn Connexion Doctrine à la base de données
* @return RunSQLResult
*/
public function runSQL($path, Connection $conn)
{
$result = new RunSQLResult();
$result->attachLogger($this->logger);
$result->setIsSuccess(true);
try {
if (is_dir($path)) {
$this->executeDir($path, $conn);
} else {
$this->executeScript($path, $conn);
}
} catch (DBALException $e) {
$result->setIsSuccess(false);
$result->setException($e);
}
$result->setEndDate(date_create('now'));
return $result;
}
/**
* Exécute dans une nouvelle transaction chaque script SQL présent dans le répertoire spécifié.
*
* @param string $dirpath
* @param Connection $conn
* @return array
* @throws DBALException
*/
private function executeDir($dirpath, Connection $conn)
{
$this->logger->info("Lecture du répertoire '$dirpath'...");
// normalize paths
$paths = Glob::glob($dirpath . '/*.[sS][qQ][lL]');
if (empty($paths)) {
throw new RuntimeException("Le répertoire '$dirpath' ne contient aucun script *.[sS][qQ][lL]");
}
foreach ($paths as $p) {
if (is_dir($p)) {
//throw new RuntimeException("Le répertoire '$dirpath' ne doit pas contenir de sous-répertoire");
continue;
}
$this->executeScript($p, $conn);
}
return $paths;
}
/**
* Exécute dans une nouvelle transaction toutes les instructions d'un script SQL.
*
* @param string $scriptPath
* @param Connection $conn
* @return array
* @throws DBALException
*/
private function executeScript($scriptPath, Connection $conn)
{
if (is_dir($scriptPath)) {
throw new RuntimeException("Le fichier '$scriptPath' spécifié est un répertoire");
}
if (!is_readable($scriptPath)) {
throw new RuntimeException("Le fichier '$scriptPath' n'est pas accessible");
}
$scriptContent = file_get_contents($scriptPath);
$queries = array_filter(array_map('trim', explode('/', $scriptContent)));
$this->logger->info("+ Exécution du script '$scriptPath'.");
$this->logger->info(sprintf("'--> %d requête(s) SQL trouvée(s).", count($queries)));
$conn->beginTransaction();
try {
foreach ($queries as $q) {
$conn->executeQuery($q);
}
$conn->commit();
} catch (DBALException $e) {
$conn->rollBack();
throw $e;
}
return $queries;
}
}
\ No newline at end of file
<?php
namespace UnicaenApp\Service\SQL;
trait RunSQLServiceAwareTrait
{
/**
* @var RunSQLService
*/
protected $runSQLService;
/**
* @param RunSQLService $runSQLService
* @return self
*/
public function setRunSQLService(RunSQLService $runSQLService)
{
$this->runSQLService = $runSQLService;
return $this;
}
}
\ No newline at end of file
<?php
namespace UnicaenApp\Service\SQL;
use Zend\Log\Logger;