File: /var/www/heifetz/heifetz-app/crons/AbstractCron.php
<?php
declare(strict_types=1);
namespace Crons;
use App\Exceptions\CrmException;
use Core\DbLib\AbstractDbDriver;
use Core\DbLib\DbDriver;
use Helpers\Converter;
use Helpers\FileHelper;
use Helpers\Output;
use Helpers\StringHelper;
use Models\CronStatistic;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Throwable;
abstract class AbstractCron
{
const EXIT_CODE_OK = 0;
const EXIT_CODE_UNSPECIFIED_ERROR = 1;
const FONT_GREEN = "\033[32m\033[1m";
const FONT_LIGHT_RED = "\033[91m\033[1m";
const NORMAL = "\033[0m";
const CRONS_LOCK_DIR = ROOT . '/tmp/crons';
static array $help = [];
/** @var AbstractDbDriver $db */
protected AbstractDbDriver $db;
protected string $className;
private Logger $logger;
private array $logData = [];
protected bool $debug = false;
protected bool $verbose = false;
protected string $title = '';
protected bool $showOutput = false;
protected bool $stopCron = false;
protected string $cronError = '';
protected bool $echo = true;
protected bool $needLock = false;
protected bool $needEchoLock = true;
protected bool $onlyForProd = false;
protected bool $allCompanies = true;
protected ?string $disableReason = null;
public function __construct()
{
$this->db = DbDriver::getInstance();
$this->className = StringHelper::getClassName(static::class);
$this->logger = new Logger('crons');
$fileName = ROOT . '/logs/' . date('Y-m-d_') . $this->className;
$this->logger->pushHandler(new StreamHandler($fileName . '.log'));
$this->logger->pushHandler(new StreamHandler($fileName . '.err.log', Logger::ERROR));
}
protected function disableMemoryLimit(): void
{
ini_set('memory_limit', '-1');
}
public function getTitle(): string
{
return $this->title;
}
public function disableOutput(): static
{
$this->showEcho(false)->showOutput(false);
return $this;
}
public function showOutput($show = true): static
{
$this->showOutput = $show;
return $this;
}
public function showEcho($show = true): static
{
$this->echo = $show;
return $this;
}
public function setVerbose(bool $verbose = false): static
{
$this->verbose = $verbose;
return $this;
}
public function setDebug(?bool $debug = true): static
{
$this->debug = $debug ?? false;
return $this;
}
public function writeLog(string $message): void
{
$this->logger->info($message, $this->logData);
}
protected function setLogDataFields(string $field, $value): static
{
$this->logData[$field] = $value;
return $this;
}
protected function removeLogDataFields(string $field): static
{
unset($this->logData[$field]);
return $this;
}
abstract public function execute();
protected function title(string $message): void
{
if ($this->showOutput) {
Output::title($message);
}
}
protected function log(string $message): void
{
if ($this->showOutput) {
Output::log($message);
} elseif ($this->echo) {
echo $message . PHP_EOL;
}
}
protected function greenLn(string $message): void
{
if ($this->showOutput) {
Output::greenLn($message);
} elseif ($this->echo) {
echo $message . PHP_EOL;
}
}
protected function write(string $message = ''): void
{
if ($this->showOutput) {
Output::write($message);
} elseif ($this->echo) {
echo ' ' . $message . PHP_EOL;
}
}
protected function line(): void
{
if ($this->showOutput) {
Output::line();
}
}
protected function lineLn(): void
{
if ($this->showOutput) {
Output::line();
Output::write(PHP_EOL);
}
}
protected function header(string $message): void
{
if ($this->showOutput) {
Output::header($message);
}
}
protected function subHeader(string $message): void
{
if ($this->showOutput) {
Output::subHeader($message);
}
}
protected function warning(string $message): void
{
if ($this->showOutput) {
Output::warning($message);
}
}
protected function redLn(string $message): void
{
if ($this->showOutput) {
Output::redLn($message);
}
}
protected function success(string $message): void
{
if ($this->showOutput) {
Output::success($message);
}
}
protected function error(string $message): void
{
if ($this->showOutput) {
Output::error($message);
}
}
protected function percent($percent): void
{
if ($this->showOutput) {
Output::percent($percent);
}
}
protected function progressBar(float $percent): void
{
if ($this->showOutput) {
$sizes = explode(' ', trim(shell_exec('stty size')));
$length = (int) $sizes[1];
echo "\r" . str_repeat(' ', $length);
$percents = round(($length - 10) / 100 * $percent);
$progressBar = "\r " . self::FONT_GREEN;
$progressBar .= str_repeat('█', intval($percents) + 1);
$progressBar .= str_repeat('▒', intval(max($length - 10 - $percents, 0)));
echo $progressBar . ' ' . $percent . '%' . self::NORMAL;
}
}
/**
* @param string $name
* @param bool $echo
*
* @return void
* @deprecated use $this->withLock()
*/
public static function lock(string $name, bool $echo = true): void
{
$running = exec('ps aux|grep ' . $name . "|grep -v grep|grep -v '/bin/sh'|wc -l");
if ($running > 1) {
if ($echo) {
Output::log('Another instance running. Exit!');
}
exit;
}
}
public static function help(): void
{
if (empty(static::$help)) {
return;
}
$maxKeySize = 0;
foreach (static::$help as $key => $description) {
$keys = explode(',', $key);
$len = strlen($key);
$len += 1 === count($keys) ? 4 : 1;
$maxKeySize = max($maxKeySize, $len);
}
$maxKeySize += 2;
Output::subHeader('Доступные опции:');
foreach (static::$help as $key => $description) {
$keys = explode(',', $key);
$key = 1 === count($keys) ? ' ' . $keys[0] : $keys[0] . ', ' . $keys[1];
Output::green(' ' . $key);
echo str_pad($description, $maxKeySize - strlen($key) + strlen($description), pad_type: STR_PAD_LEFT) . PHP_EOL;
}
Output::write('');
}
public function run(): void
{
if ($this->needLock) {
try {
FileHelper::checkAndCreateDir(self::CRONS_LOCK_DIR);
} catch (Throwable) {
$this->logger->error('Невозможно создать каталог для Lock файлов', $this->logData);
throw new CrmException('Невозможно создать каталог для Lock файлов');
}
$cronName = StringHelper::getClassName(static::class);
$lockFilePath = self::CRONS_LOCK_DIR . '/' . $cronName . '.lock';
$lockFile = file_exists($lockFilePath);
if (!empty($lockFile)) {
if ($this->needEchoLock) {
$this->log('Другой аналогичный процесс уже запущен');
}
return;
}
try {
file_put_contents($lockFilePath, 1);
} catch (Throwable) {
$this->logger->error('Невозможно создать Lock файл', $this->logData);
throw new CrmException('Невозможно создать Lock файл');
}
}
$this->showOutput = true;
if (!is_null($this->disableReason)) {
$this->warning($this->disableReason);
return;
}
$time = microtime(true);
$this->title(date('d.m.Y H:i:s') . ': ' . $this->getTitle());
if ($this->onlyForProd && !IS_PROD) {
$this->warning('Не работает на тесте');
$this->success('Готово');
return;
}
if (!$this->stopCron) {
try {
static::execute();
$this->write();
} catch (Throwable $exception) {
$error = '';
if (IS_DEV || IS_TEST) {
$error = $exception->getFile() . ':' . $exception->getLine() . PHP_EOL;
$error .= $exception->getTraceAsString() . PHP_EOL;
}
$this->error($error . $exception->getMessage());
$this->logger->error($error, $this->logData);
}
}
$time = (int) round(microtime(true) - $time);
$this->write();
$text = 'Затраченное время: ' . Converter::secondsToHumanReadable($time);
$this->logData['_time'] = $time;
$this->log($text);
$memory = memory_get_peak_usage();
$this->log('Максимально было затрачено памяти: ' . Converter::getHumanReadableMem($memory));
$this->logData['_memory'] = $memory;
CronStatistic::addStatistic($memory, $time, $this->className);
if ($this->stopCron) {
$error = empty($this->cronError) ? 'Крон выполнился не до конца' : $this->cronError;
$this->logger->error($error, $this->logData);
$this->error($error);
} else {
$this->success('Готово');
}
if ($this->needLock && !empty($lockFilePath) && file_exists($lockFilePath)) {
unlink($lockFilePath);
}
$this->logger->info('Команда успешно выполнена', $this->logData);
}
}