diff --git a/src/frameworks/yii2/http_request.php b/src/frameworks/yii2/http_request.php new file mode 100644 index 0000000..5a69bf9 --- /dev/null +++ b/src/frameworks/yii2/http_request.php @@ -0,0 +1,788 @@ +position = 0; + $this->isInput = ($path === 'php://input'); + $this->buffer = ''; + return true; + } + + public function stream_read($count) + { + $dataSource = $this->isInput ? ($GLOBALS['__DDLESS_RAW_INPUT__'] ?? '') : $this->buffer; + $data = substr($dataSource, $this->position, $count); + $this->position += strlen($data); + return $data; + } + + public function stream_eof() + { + $dataSource = $this->isInput ? ($GLOBALS['__DDLESS_RAW_INPUT__'] ?? '') : $this->buffer; + return $this->position >= strlen($dataSource); + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_seek($offset, $whence = SEEK_SET) + { + $dataSource = $this->isInput ? ($GLOBALS['__DDLESS_RAW_INPUT__'] ?? '') : $this->buffer; + $length = strlen($dataSource); + + switch ($whence) { + case SEEK_SET: + $target = $offset; + break; + case SEEK_CUR: + $target = $this->position + $offset; + break; + case SEEK_END: + $target = $length + $offset; + break; + default: + return false; + } + + if ($target < 0) { + return false; + } + + $this->position = $target; + return true; + } + + public function stream_stat() + { + $size = $this->isInput ? strlen($GLOBALS['__DDLESS_RAW_INPUT__'] ?? '') : strlen($this->buffer); + return ['size' => $size]; + } + + public function stream_write($data) + { + if ($this->isInput) { + return strlen($data); + } + + $len = strlen($data); + $before = substr($this->buffer, 0, $this->position); + $after = substr($this->buffer, $this->position + $len); + $this->buffer = $before . $data . $after; + $this->position += $len; + return $len; + } + + public function stream_truncate(int $newSize) + { + if ($this->isInput) { + return false; + } + if ($newSize < strlen($this->buffer)) { + $this->buffer = substr($this->buffer, 0, $newSize); + } else { + $this->buffer = str_pad($this->buffer, $newSize, "\0"); + } + if ($this->position > $newSize) { + $this->position = $newSize; + } + return true; + } + } +} + +function ddless_normalize_relative_path(string $path): string +{ + $normalized = str_replace('\\', '/', $path); + $root = defined('DDLESS_PROJECT_ROOT') ? str_replace('\\', '/', (string)DDLESS_PROJECT_ROOT) : null; + + if ($root && str_starts_with($normalized, $root)) { + $trimmed = substr($normalized, strlen($root)); + return ltrim($trimmed, '/'); + } + + return ltrim($normalized, '/'); +} + +function ddless_normalize_headers(array $headers): array +{ + $normalized = []; + foreach ($headers as $key => $value) { + if (is_array($value)) { + $normalized[$key] = array_map( + static fn($entry) => is_scalar($entry) ? (string)$entry : json_encode($entry), + $value, + ); + } elseif ($value !== null) { + $normalized[$key] = (string)$value; + } + } + return $normalized; +} + +if (!function_exists('ddless_normalize_value')) { + function ddless_normalize_value($value, int $depth = 0) + { + $maxDepth = $GLOBALS['__DDLESS_SERIALIZE_DEPTH__'] ?? 4; + if ($depth > $maxDepth) { + if (is_object($value)) { + return '[object ' . get_class($value) . ']'; + } + return is_scalar($value) ? $value : '[max-depth]'; + } + + if (is_null($value) || is_bool($value) || is_int($value) || is_float($value)) { + return $value; + } + + if (is_string($value)) { + if (strlen($value) > 10000) { + return substr($value, 0, 10000) . "\xe2\x80\xa6"; + } + return $value; + } + + if (is_array($value)) { + $result = []; + foreach ($value as $key => $item) { + $result[$key] = ddless_normalize_value($item, $depth + 1); + } + return $result; + } + + if ($value instanceof \JsonSerializable) { + try { + return ddless_normalize_value($value->jsonSerialize(), $depth + 1); + } catch (\Throwable $exception) { + return '[object ' . get_class($value) . ']'; + } + } + + if (method_exists($value, 'toArray')) { + try { + return ddless_normalize_value($value->toArray(), $depth + 1); + } catch (\Throwable $exception) { + } + } + + if ($value instanceof \DateTimeInterface) { + return $value->format(\DateTimeInterface::ATOM); + } + + if (is_object($value)) { + return '[object ' . get_class($value) . ']'; + } + + if (is_resource($value)) { + return '[resource ' . (get_resource_type($value) ?: 'resource') . ']'; + } + + return (string)$value; + } +} + +if (!function_exists('ddless_build_call_stack')) { + function ddless_build_call_stack(array $backtrace): array + { + $result = []; + $limit = 30; + + foreach ($backtrace as $index => $frame) { + if ($index >= $limit) { + $result[] = ['label' => null, 'file' => null, 'line' => null, 'truncated' => true]; + break; + } + if (!is_array($frame)) { + continue; + } + + $class = isset($frame['class']) && is_string($frame['class']) ? $frame['class'] : null; + $type = isset($frame['type']) && is_string($frame['type']) ? $frame['type'] : ''; + $function = isset($frame['function']) && is_string($frame['function']) ? $frame['function'] : null; + $file = isset($frame['file']) && is_string($frame['file']) ? ddless_normalize_relative_path($frame['file']) : null; + $line = isset($frame['line']) && (is_int($frame['line']) || ctype_digit((string)$frame['line'])) ? (int)$frame['line'] : null; + + $label = $function; + if ($class !== null && $function !== null) { + $label = $class . $type . $function; + } elseif ($function === null) { + $label = 'main'; + } + + $result[] = ['label' => $label, 'file' => $file, 'line' => $line]; + } + + return $result; + } +} + +function ddless_read_breakpoints(string $ddlessDir): array +{ + $path = $ddlessDir . DIRECTORY_SEPARATOR . 'breakpoints.json'; + if (!is_file($path)) { + return []; + } + + try { + $raw = file_get_contents($path); + if ($raw === false) { + return []; + } + $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($decoded)) { + return []; + } + $result = []; + foreach ($decoded as $file => $lines) { + if (!is_array($lines)) { + continue; + } + $normalizedFile = ddless_normalize_relative_path((string)$file); + $normalizedLines = []; + foreach ($lines as $line) { + if (is_numeric($line)) { + $normalizedLines[] = (int)$line; + } + } + if ($normalizedFile !== '' && $normalizedLines) { + $result[$normalizedFile] = array_values(array_unique($normalizedLines)); + } + } + return $result; + } catch (\Throwable $exception) { + fwrite(STDERR, "[ddless] Failed to read breakpoints: {$exception->getMessage()}\n"); + return []; + } +} + +function ddless_collect_logs(string $logDir): array +{ + $candidates = ['app.log']; + $logFilePath = null; + + foreach ($candidates as $candidate) { + $path = $logDir . DIRECTORY_SEPARATOR . $candidate; + if (is_file($path)) { + $logFilePath = $path; + break; + } + } + + if ($logFilePath === null || !is_readable($logFilePath)) { + return []; + } + + $handle = fopen($logFilePath, 'rb'); + if ($handle === false) { + return []; + } + + $newSize = filesize($logFilePath); + $start = 0; + if ($newSize > 32768) { + $start = $newSize - 32768; + } + + if ($start > 0) { + fseek($handle, $start); + fgets($handle); + } + + $data = stream_get_contents($handle) ?: ''; + fclose($handle); + + $data = trim($data); + if ($data === '') { + return []; + } + + $lines = preg_split("/\r?\n/", $data); + if (!$lines) { + return []; + } + + return array_slice(array_map('strval', $lines), -200); +} + +function ddless_encode_json($value): string +{ + $encoded = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + if ($encoded === false) { + return "{}"; + } + return $encoded; +} + +function ddless_prepare_body_payload(?string $content, ?string $contentType): array +{ + $raw = $content ?? ''; + $encoding = 'utf8'; + $truncated = false; + + $isTextual = $contentType === null + || preg_match('/json|text|xml|javascript|css|html|csv|urlencoded/i', $contentType); + + if (!$isTextual) { + $encoding = 'base64'; + $prepared = base64_encode($raw); + } else { + if ($raw !== '' && function_exists('mb_check_encoding') && !mb_check_encoding($raw, 'UTF-8')) { + $encoding = 'base64'; + $prepared = base64_encode($raw); + } else { + if (strlen($raw) > 131072) { + $prepared = substr($raw, 0, 131072); + $truncated = true; + } else { + $prepared = $raw; + } + } + } + + return [ + 'content' => $prepared, + 'encoding' => $encoding, + 'truncated' => $truncated, + ]; +} + +function ddless_response_status_text(int $statusCode): ?string +{ + static $map = [ + 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', + 203 => 'Non-Authoritative Information', 204 => 'No Content', + 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', + 303 => 'See Other', 304 => 'Not Modified', + 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', + 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', + 411 => 'Length Required', 412 => 'Precondition Failed', + 413 => 'Payload Too Large', 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', 422 => 'Unprocessable Entity', + 429 => 'Too Many Requests', + 500 => 'Internal Server Error', 501 => 'Not Implemented', + 502 => 'Bad Gateway', 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', + ]; + + return $map[$statusCode] ?? null; +} + +// Yii 2 Bootstrap +$projectRoot = !empty($GLOBALS['__DDLESS_PLAYGROUND__']) && defined('DDLESS_PROJECT_ROOT') + ? DDLESS_PROJECT_ROOT + : dirname(__DIR__, 3); +$ddlessDirectory = dirname(__DIR__, 2); + +if (!defined('DDLESS_PROJECT_ROOT')) { + define('DDLESS_PROJECT_ROOT', $projectRoot); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// Proxy mode (php -S / cli-server): include web/index.php directly. +// Yii2's entry point handles bootstrap, routing, and response natively. +// ═══════════════════════════════════════════════════════════════════════════════ + +if (php_sapi_name() === 'cli-server') { + register_shutdown_function( + function () use ($projectRoot, $ddlessDirectory) { + $statusCode = http_response_code() ?: 200; + + $responseHeaders = []; + foreach (headers_list() as $headerLine) { + $colonPos = strpos($headerLine, ':'); + if ($colonPos !== false) { + $name = trim(substr($headerLine, 0, $colonPos)); + $value = trim(substr($headerLine, $colonPos + 1)); + $responseHeaders[$name] = $value; + } + } + + $snapshot = [ + 'timestamp' => date('c'), + 'sessionId' => null, + 'request' => [ + 'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET', + 'path' => parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/', + 'fullUrl' => ($_SERVER['REQUEST_SCHEME'] ?? 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . ($_SERVER['REQUEST_URI'] ?? '/'), + 'headers' => [], + 'query' => $_GET ?? [], + 'input' => $_POST ?? [], + 'cookies' => $_COOKIE ?? [], + ], + 'response' => [ + 'status' => $statusCode, + 'statusText' => null, + 'headers' => $responseHeaders, + 'body' => ['content' => '', 'encoding' => 'utf8', 'truncated' => false], + ], + 'context' => [ + 'route' => null, 'controller' => null, + 'breakpoints' => [], 'hitBreakpoints' => [], + 'variables' => [], 'callStack' => [], + ], + 'metrics' => [ + 'durationMs' => 0, + 'memoryPeakBytes' => memory_get_peak_usage(true), + ], + 'logs' => [], + ]; + + $encoded = json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + + if (function_exists('ddless_get_session_dir')) { + @file_put_contents(ddless_get_session_dir() . DIRECTORY_SEPARATOR . 'last_execution.json', $encoded); + } + @file_put_contents($ddlessDirectory . DIRECTORY_SEPARATOR . 'last_execution.json', $encoded); + + fwrite(STDERR, "__DDLESS_REQUEST_COMPLETE__\n"); + } + ); + + if (getenv('DDLESS_DEBUG_MODE') === 'true') { + $debugModule = dirname(__DIR__, 2) . '/debug.php'; + if (file_exists($debugModule)) { + require_once $debugModule; + if (function_exists('ddless_register_stream_wrapper')) { + ddless_register_stream_wrapper(); + } + } + } + + fwrite(STDERR, "[ddless] Starting Yii2 request handling...\n"); + + $indexFile = $projectRoot . '/web/index.php'; + if (!is_file($indexFile)) { + http_response_code(500); + echo "DDLess: Yii2 web/index.php not found."; + exit(1); + } + + require $indexFile; + exit; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// CLI mode (DDLess direct request): manual bootstrap, capture response, snapshot. +// ═══════════════════════════════════════════════════════════════════════════════ + +// YII_ENV_TEST makes Yii2 throw ExitException instead of calling exit(), +// allowing our code to continue after $app->run() for snapshot generation. +if (!defined('YII_ENV_TEST')) { + define('YII_ENV_TEST', true); +} + +require_once $projectRoot . '/vendor/autoload.php'; + +$yiiFile = $projectRoot . '/vendor/yiisoft/yii2/Yii.php'; +if (is_file($yiiFile)) { + require_once $yiiFile; +} + +if (!class_exists('Yii')) { + fwrite(STDERR, "[ddless] Yii class not found. Run composer install.\n"); + exit(1); +} + +$configFile = $projectRoot . '/config/web.php'; +if (!is_file($configFile)) { + fwrite(STDERR, "[ddless] Yii2 config/web.php not found.\n"); + exit(1); +} + +$config = require $configFile; + +$breakpoints = ddless_read_breakpoints($ddlessDirectory); +$executionStartedAt = microtime(true); +$logDir = $projectRoot . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'logs'; + +$phpWrapperReplaced = false; +if (in_array('php', stream_get_wrappers(), true)) { + $phpWrapperReplaced = @stream_wrapper_unregister('php'); + if ($phpWrapperReplaced) { + if (!stream_wrapper_register('php', 'DDLessPhpInputStream')) { + stream_wrapper_restore('php'); + $phpWrapperReplaced = false; + } + } +} + +if (getenv('DDLESS_DEBUG_MODE') === 'true') { + if (function_exists('ddless_register_stream_wrapper')) { + ddless_register_stream_wrapper(); + } +} + +// Pre-load instrumented files through the wrapper +if (getenv('DDLESS_DEBUG_MODE') === 'true' && !empty($GLOBALS['__DDLESS_INSTRUMENTED_CODE__'])) { + foreach (array_keys($GLOBALS['__DDLESS_INSTRUMENTED_CODE__']) as $instrumentedPath) { + if (is_file($instrumentedPath) && str_ends_with($instrumentedPath, '.php')) { + include_once $instrumentedPath; + } + } +} + +try { + fwrite(STDERR, "[ddless] Starting Yii2 request handling...\n"); + + $app = new \yii\web\Application($config); + + ob_start(); + try { + $app->run(); + } catch (\yii\base\ExitException $e) { + // Yii2 throws ExitException (instead of exit) when YII_ENV_TEST is true + } + $bodyOutput = ob_get_clean(); + + $response = $app->getResponse(); + $request = $app->getRequest(); + + $statusCode = $response->getStatusCode(); + $responseContentType = null; + foreach ($response->getHeaders() as $name => $values) { + if (strtolower($name) === 'content-type') { + $responseContentType = is_array($values) ? reset($values) : $values; + break; + } + } + + $statusText = $response->statusText ?: (ddless_response_status_text($statusCode) ?? 'OK'); + + echo sprintf("HTTP/1.1 %d %s\r\n", $statusCode, $statusText); + + foreach ($response->getHeaders() as $name => $values) { + if (!is_array($values)) { + $values = [$values]; + } + foreach ($values as $value) { + echo sprintf("%s: %s\r\n", $name, $value); + } + } + + echo "\r\n"; + + $capturedBodyContent = $response->content ?: $bodyOutput; + if (is_string($capturedBodyContent)) { + echo $capturedBodyContent; + } + + fwrite(STDERR, "[ddless] Request handling completed, status: " . $statusCode . "\n"); + + // Snapshot Generation + $executionFinishedAt = microtime(true); + $durationMs = ($executionFinishedAt - $executionStartedAt) * 1000; + $memoryPeakBytes = memory_get_peak_usage(true); + + $rawInput = $GLOBALS['__DDLESS_RAW_INPUT__'] ?? ''; + $contentType = $_SERVER['CONTENT_TYPE'] ?? null; + + $requestHeaders = []; + if (method_exists($request, 'getHeaders')) { + foreach ($request->getHeaders() as $name => $values) { + $requestHeaders[$name] = is_array($values) ? implode(', ', $values) : $values; + } + } + + $requestSummary = [ + 'method' => $request->getMethod(), + 'path' => $request->getUrl(), + 'fullUrl' => $request->getAbsoluteUrl(), + 'headers' => ddless_normalize_headers($requestHeaders), + 'query' => ddless_normalize_value($request->get() ?? []), + 'input' => ddless_normalize_value($request->post() ?? []), + 'cookies' => ddless_normalize_value($_COOKIE ?? []), + 'rawBody' => ddless_prepare_body_payload($rawInput, $contentType), + ]; + + $snapshotResponseHeaders = []; + foreach ($response->getHeaders() as $name => $values) { + $snapshotResponseHeaders[$name] = is_array($values) ? implode(', ', $values) : $values; + } + + $responseBodyPayload = ddless_prepare_body_payload( + is_string($capturedBodyContent) ? $capturedBodyContent : '', + $responseContentType ?: null, + ); + + $routeInfo = null; + $controllerInfo = null; + $hitBreakpoints = []; + $callStack = []; + + try { + $route = \Yii::$app->requestedRoute ?? null; + $controller = \Yii::$app->controller ?? null; + $action = $controller ? ($controller->action ?? null) : null; + + if ($controller) { + $controllerClass = get_class($controller); + $actionId = $action ? $action->id : 'index'; + $actionMethod = 'action' . str_replace(' ', '', ucwords(str_replace('-', ' ', $actionId))); + + $routeInfo = [ + 'uri' => $requestSummary['path'], + 'name' => $route, + 'methods' => [$request->getMethod()], + 'action' => $controllerClass . '::' . $actionMethod, + 'parameters' => ddless_normalize_value($request->get() ?? []), + ]; + + $controllerFile = null; + $startLine = null; + $endLine = null; + + if (method_exists($controllerClass, $actionMethod)) { + $reflection = new \ReflectionMethod($controllerClass, $actionMethod); + $controllerFile = $reflection->getFileName() ?: null; + $startLine = $reflection->getStartLine() ?: null; + $endLine = $reflection->getEndLine() ?: null; + } + + if ($controllerFile) { + $relativeControllerPath = ddless_normalize_relative_path( + str_starts_with($controllerFile, $projectRoot) + ? substr($controllerFile, strlen($projectRoot) + 1) + : $controllerFile + ); + + if ($relativeControllerPath && isset($breakpoints[$relativeControllerPath])) { + foreach ($breakpoints[$relativeControllerPath] as $line) { + if ($startLine !== null && $endLine !== null && $line >= $startLine && $line <= $endLine) { + $hitBreakpoints[$relativeControllerPath][] = $line; + } + } + } + + $controllerInfo = [ + 'file' => $relativeControllerPath, + 'class' => $controllerClass, + 'method' => $actionMethod, + 'startLine' => $startLine, + 'endLine' => $endLine, + ]; + + if (!empty($hitBreakpoints[$relativeControllerPath])) { + sort($hitBreakpoints[$relativeControllerPath]); + } + } + + $callStack[] = [ + 'label' => $routeInfo['action'], + 'file' => $controllerInfo['file'] ?? null, + 'line' => $controllerInfo['startLine'] ?? null, + ]; + } + } catch (\Throwable $exception) { + } + + $variables = [ + 'routeParameters' => $routeInfo['parameters'] ?? [], + 'requestInput' => $requestSummary['input'], + 'query' => $requestSummary['query'], + ]; + + try { + $session = \Yii::$app->has('session') ? \Yii::$app->session : null; + if ($session && $session->getIsActive()) { + $variables['session'] = ddless_normalize_value($session->toArray()); + } + } catch (\Throwable $exception) { + } + + try { + $user = \Yii::$app->has('user') ? \Yii::$app->user : null; + if ($user && !$user->getIsGuest()) { + $identity = $user->getIdentity(); + if ($identity) { + $variables['user'] = ddless_normalize_value($identity->toArray()); + } + } + } catch (\Throwable $exception) { + } + + $logs = ddless_collect_logs($logDir); + + $snapshot = [ + 'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM), + 'sessionId' => null, + 'request' => $requestSummary, + 'response' => [ + 'status' => $statusCode, + 'statusText' => ddless_response_status_text($statusCode), + 'headers' => ddless_normalize_headers($snapshotResponseHeaders), + 'body' => $responseBodyPayload, + ], + 'context' => [ + 'route' => $routeInfo, + 'controller' => $controllerInfo, + 'breakpoints' => $breakpoints, + 'hitBreakpoints' => $hitBreakpoints, + 'variables' => $variables, + 'callStack' => $callStack, + ], + 'metrics' => [ + 'durationMs' => round($durationMs, 4), + 'memoryPeakBytes' => $memoryPeakBytes, + ], + 'logs' => $logs, + ]; + + $encodedSnapshot = ddless_encode_json($snapshot) . "\n"; + + if (function_exists('ddless_get_session_dir')) { + @file_put_contents(ddless_get_session_dir() . DIRECTORY_SEPARATOR . 'last_execution.json', $encodedSnapshot); + } + + $rootSnapshotPath = $ddlessDirectory . DIRECTORY_SEPARATOR . 'last_execution.json'; + @file_put_contents($rootSnapshotPath, $encodedSnapshot); + + fwrite(STDERR, "[ddless] Request completed successfully\n"); +} finally { + if ($phpWrapperReplaced) { + @stream_wrapper_restore('php'); + } + fwrite(STDERR, "__DDLESS_REQUEST_COMPLETE__\n"); +} diff --git a/src/frameworks/yii2/method_executor.php b/src/frameworks/yii2/method_executor.php new file mode 100644 index 0000000..600ce78 --- /dev/null +++ b/src/frameworks/yii2/method_executor.php @@ -0,0 +1,382 @@ + false, + 'error' => $message, + 'context' => $context, + 'timestamp' => date('c'), + ]; + fwrite(STDOUT, json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); + exit(1); +} + +function ddless_method_success(array $data): void +{ + $output = array_merge([ + 'ok' => true, + 'timestamp' => date('c'), + ], $data); + fwrite(STDOUT, json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); + exit(0); +} + +function ddless_clean_php_code(string $code): string +{ + $code = trim($code); + if (str_starts_with($code, '')) { + $code = substr($code, 0, -2); + } + return trim($code); +} + +function ddless_serialize_value($value, int $depth = 0, int $maxDepth = 10, int $maxArrayItems = 100) +{ + if ($depth > $maxDepth) { + if (is_object($value)) return ['__type' => 'object', '__class' => get_class($value), '__truncated' => true]; + if (is_array($value)) return ['__type' => 'array', '__truncated' => true, '__count' => count($value)]; + return $value; + } + + if (is_null($value) || is_bool($value) || is_int($value) || is_float($value)) return $value; + + if (is_string($value)) { + if (strlen($value) > 10000) return substr($value, 0, 10000) . "\xe2\x80\xa6 [truncated]"; + return $value; + } + + if (is_array($value)) { + $result = []; + $count = 0; + foreach ($value as $key => $item) { + if ($count >= $maxArrayItems) { + $result['__truncated'] = true; + $result['__totalItems'] = count($value); + break; + } + $result[$key] = ddless_serialize_value($item, $depth + 1, $maxDepth, $maxArrayItems); + $count++; + } + return $result; + } + + if ($value instanceof \DateTimeInterface) { + return [ + '__type' => 'datetime', + 'formatted' => $value->format('Y-m-d H:i:s'), + 'timezone' => $value->getTimezone()->getName(), + 'iso8601' => $value->format(\DateTimeInterface::ATOM), + ]; + } + + if ($value instanceof \JsonSerializable) { + try { + return ddless_serialize_value($value->jsonSerialize(), $depth + 1, $maxDepth, $maxArrayItems); + } catch (\Throwable $e) { + return ['__type' => 'object', '__class' => get_class($value), '__error' => $e->getMessage()]; + } + } + + if (method_exists($value, 'toArray')) { + try { + return ddless_serialize_value($value->toArray(), $depth + 1, $maxDepth, $maxArrayItems); + } catch (\Throwable $e) { + } + } + + if (is_object($value)) { + $result = ['__type' => 'object', '__class' => get_class($value)]; + try { + $ref = new \ReflectionClass($value); + $props = $ref->getProperties(); + foreach ($props as $prop) { + $prop->setAccessible(true); + if ($prop->isInitialized($value)) { + $result[$prop->getName()] = ddless_serialize_value($prop->getValue($value), $depth + 1, $maxDepth, $maxArrayItems); + } + } + } catch (\Throwable $e) { + $result['__error'] = $e->getMessage(); + } + return $result; + } + + if (is_resource($value)) { + return ['__type' => 'resource', '__resourceType' => get_resource_type($value)]; + } + + return (string)$value; +} + +$inputJson = ''; + +if (!empty($GLOBALS['__DDLESS_METHOD_INPUT__'])) { + $inputJson = $GLOBALS['__DDLESS_METHOD_INPUT__']; +} elseif (($inputFile = getenv('DDLESS_METHOD_INPUT_FILE')) && $inputFile !== '__ALREADY_LOADED__' && is_file($inputFile)) { + $inputJson = file_get_contents($inputFile); + @unlink($inputFile); +} else { + while (!feof(STDIN)) { + $line = fgets(STDIN); + if ($line === false) break; + $inputJson .= $line; + } +} + +$input = json_decode(trim($inputJson), true); +if (!is_array($input)) { + ddless_method_error('Invalid input JSON'); +} + +$targetClass = $input['class'] ?? ''; +$targetMethod = $input['method'] ?? null; +$parameterCode = $input['parameterCode'] ?? 'return [];'; +$constructorCode = $input['constructorCode'] ?? 'return [];'; + +if (empty($targetMethod)) { + ddless_method_error('Missing required field: method/function name'); +} + +$composerAutoload = DDLESS_PROJECT_ROOT . '/vendor/autoload.php'; +if (!file_exists($composerAutoload)) { + ddless_method_error('Composer autoload not found', ['path' => $composerAutoload]); +} + +if (getenv('DDLESS_DEBUG_MODE') === 'true') { + $debugModule = dirname(__DIR__, 2) . '/debug.php'; + if (file_exists($debugModule)) { + require_once $debugModule; + if (function_exists('ddless_register_stream_wrapper')) { + fwrite(STDERR, "[ddless] Registering stream wrapper BEFORE autoload...\n"); + ddless_register_stream_wrapper(); + } + } +} + +fwrite(STDERR, "[ddless] Loading Composer autoload...\n"); +require $composerAutoload; +fwrite(STDERR, "[ddless] Composer autoload loaded.\n"); + +$yiiFile = DDLESS_PROJECT_ROOT . '/vendor/yiisoft/yii2/Yii.php'; +if (is_file($yiiFile)) { + require_once $yiiFile; +} + +$configFile = DDLESS_PROJECT_ROOT . '/config/web.php'; +if (!is_file($configFile)) { + $configFile = DDLESS_PROJECT_ROOT . '/config/console.php'; +} +if (!is_file($configFile)) { + ddless_method_error('Yii2 config not found (tried config/web.php and config/console.php)'); +} + +$config = require $configFile; + +try { + fwrite(STDERR, "[ddless] Booting Yii2 application...\n"); + $app = new \yii\web\Application($config); + fwrite(STDERR, "[ddless] Yii2 application booted successfully.\n"); +} catch (\Throwable $e) { + ddless_method_error('Failed to boot Yii2 application: ' . $e->getMessage(), [ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => array_slice($e->getTrace(), 0, 5), + ]); +} + +$cleanedConstructorCode = ddless_clean_php_code($constructorCode); +$cleanedParameterCode = ddless_clean_php_code($parameterCode); + +$constructorParameters = []; +$methodParameters = []; + +try { + $constructorParameters = (function() use ($cleanedConstructorCode) { + $result = eval($cleanedConstructorCode); + if (!is_array($result)) { + throw new \InvalidArgumentException('Constructor code must return an array'); + } + return $result; + })(); +} catch (\Throwable $e) { + ddless_method_error('Error executing constructor code: ' . $e->getMessage(), [ + 'code' => $cleanedConstructorCode, + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); +} + +try { + $methodParameters = (function() use ($cleanedParameterCode) { + $result = eval($cleanedParameterCode); + if (!is_array($result)) { + throw new \InvalidArgumentException('Parameter code must return an array'); + } + return $result; + })(); +} catch (\Throwable $e) { + ddless_method_error('Error executing parameter code: ' . $e->getMessage(), [ + 'code' => $cleanedParameterCode, + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); +} + +try { + $startTime = microtime(true); + $startMemory = memory_get_usage(true); + + $result = null; + $isFunction = false; + $isStatic = false; + + $isGlobalFunction = empty($targetClass) || strtolower($targetClass) === 'function' || strtolower($targetClass) === 'global'; + + if ($isGlobalFunction) { + $isFunction = true; + + if (!function_exists($targetMethod)) { + ddless_method_error("Function not found: {$targetMethod}"); + } + + $result = call_user_func_array($targetMethod, $methodParameters); + } else { + if (!class_exists($targetClass)) { + ddless_method_error("Class not found: {$targetClass}"); + } + + if (!method_exists($targetClass, $targetMethod)) { + ddless_method_error("Method not found: {$targetClass}::{$targetMethod}"); + } + + $reflection = new \ReflectionClass($targetClass); + $methodReflection = $reflection->getMethod($targetMethod); + $isStatic = $methodReflection->isStatic(); + + $instance = null; + if (!$isStatic) { + try { + if (!empty($constructorParameters)) { + $instance = $reflection->newInstanceArgs($constructorParameters); + } elseif (\Yii::$app && \Yii::$app->has($targetClass)) { + $instance = \Yii::$app->get($targetClass); + } elseif (\Yii::$container->hasSingleton($targetClass) || \Yii::$container->has($targetClass)) { + $instance = \Yii::$container->get($targetClass); + } else { + $constructor = $reflection->getConstructor(); + if ($constructor === null || $constructor->getNumberOfRequiredParameters() === 0) { + $instance = \Yii::createObject($targetClass); + } else { + ddless_method_error('Cannot instantiate class - constructor requires parameters.', [ + 'class' => $targetClass, + 'requiredParams' => $constructor->getNumberOfRequiredParameters(), + 'hint' => 'Provide constructor parameters or configure the class in Yii DI container.', + ]); + } + } + } catch (\Throwable $containerError) { + $constructor = $reflection->getConstructor(); + if ($constructor === null || $constructor->getNumberOfRequiredParameters() === 0) { + try { + $instance = $reflection->newInstance(); + } catch (\Throwable $e) { + ddless_method_error('Failed to instantiate class: ' . $containerError->getMessage(), [ + 'class' => $targetClass, + 'fallbackError' => $e->getMessage(), + ]); + } + } else { + ddless_method_error('Cannot instantiate class: ' . $containerError->getMessage(), [ + 'class' => $targetClass, + 'requiredParams' => $constructor->getNumberOfRequiredParameters(), + ]); + } + } + } + + if ($isStatic) { + $result = $methodReflection->invokeArgs(null, $methodParameters); + } else { + $result = $methodReflection->invokeArgs($instance, $methodParameters); + } + } + + $endTime = microtime(true); + $endMemory = memory_get_usage(true); + + ddless_method_success([ + 'class' => $isFunction ? null : $targetClass, + 'method' => $targetMethod, + 'type' => $isFunction ? 'function' : 'method', + 'static' => $isStatic, + 'parameters' => ddless_serialize_value($methodParameters), + 'result' => ddless_serialize_value($result), + 'metrics' => [ + 'durationMs' => round(($endTime - $startTime) * 1000, 2), + 'memoryUsedBytes' => $endMemory - $startMemory, + 'memoryPeakBytes' => memory_get_peak_usage(true), + ], + ]); +} catch (\Throwable $e) { + $endTime = microtime(true); + $endMemory = memory_get_usage(true); + + $exceptionInfo = [ + 'class' => get_class($e), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + + $appTrace = []; + foreach ($e->getTrace() as $frame) { + $file = $frame['file'] ?? ''; + if ($file && !str_contains($file, '/vendor/') && !str_contains($file, '\\vendor\\')) { + $appTrace[] = [ + 'file' => $frame['file'] ?? '', + 'line' => $frame['line'] ?? 0, + 'function' => ($frame['class'] ?? '') . ($frame['type'] ?? '') . ($frame['function'] ?? ''), + ]; + if (count($appTrace) >= 5) break; + } + } + if (!empty($appTrace)) { + $exceptionInfo['trace'] = $appTrace; + } + + ddless_method_success([ + 'class' => isset($isFunction) && $isFunction ? null : ($targetClass ?? null), + 'method' => $targetMethod ?? '', + 'type' => (isset($isFunction) && $isFunction) ? 'function' : 'method', + 'static' => $isStatic ?? false, + 'parameters' => isset($methodParameters) ? ddless_serialize_value($methodParameters) : [], + 'result' => null, + 'exception' => $exceptionInfo, + 'metrics' => [ + 'durationMs' => isset($startTime) ? round(($endTime - $startTime) * 1000, 2) : 0, + 'memoryUsedBytes' => isset($startMemory) ? ($endMemory - $startMemory) : 0, + 'memoryPeakBytes' => memory_get_peak_usage(true), + ], + ]); +} diff --git a/src/frameworks/yii2/task_runner.php b/src/frameworks/yii2/task_runner.php new file mode 100644 index 0000000..3cf9e7c --- /dev/null +++ b/src/frameworks/yii2/task_runner.php @@ -0,0 +1,413 @@ + $ok, + 'durationMs' => round((microtime(true) - $startTime) * 1000, 2), + ]; + if ($error !== null) { + $data['error'] = $error; + } + echo "__DDLESS_TASK_DONE__:" . json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; +} + +function ddless_clean_php_code(string $code): string { + $code = trim($code); + if (str_starts_with($code, '')) { + $code = substr($code, 0, -2); + } + return trim($code); +} + +function ddless_prompt_with_rule(string $promptType, string $message, array $extra, ?callable $rule): string { + $validationError = null; + + while (true) { + $data = array_merge([ + 'promptType' => $promptType, + 'message' => $message, + ], $extra); + + if ($validationError !== null) { + $data['validationError'] = $validationError; + } + + ddless_task_emit('prompt', $data); + $input = trim((string)fgets(STDIN)); + + if ($rule === null) { + return $input; + } + + $result = $rule($input); + if ($result === true || $result === null) { + return $input; + } + + $validationError = is_string($result) ? $result : 'Invalid value.'; + } +} + +class DdlessChart { + private string $chartType = 'line'; + private string $chartTitle = ''; + private array $labels = []; + private array $datasets = []; + + public function title(string $title): self { $this->chartTitle = $title; return $this; } + public function type(string $type): self { $this->chartType = $type; return $this; } + public function line(): self { $this->chartType = 'line'; return $this; } + public function bar(): self { $this->chartType = 'bar'; return $this; } + public function pie(): self { $this->chartType = 'pie'; return $this; } + public function doughnut(): self { $this->chartType = 'doughnut'; return $this; } + public function area(): self { $this->chartType = 'area'; return $this; } + + public function labels(array $labels): self { $this->labels = $labels; return $this; } + public function dataset(string $label, array $data): self { + $this->datasets[] = ['label' => $label, 'data' => $data]; + return $this; + } + + public function render(): void { + ddless_task_emit('chart', [ + 'title' => $this->chartTitle, + 'chartType' => $this->chartType, + 'labels' => $this->labels, + 'datasets' => $this->datasets, + ]); + } +} + +$inputJson = $GLOBALS['__DDLESS_TASK_INPUT__'] ?? null; +if (!$inputJson) { + ddless_task_emit('error', ['message' => 'Task input not found.']); + ddless_task_done(false, $taskStartTime, 'Task input not found.'); + exit(1); +} + +$input = json_decode($inputJson, true); +if (!is_array($input)) { + ddless_task_emit('error', ['message' => 'Invalid task input JSON.']); + ddless_task_done(false, $taskStartTime, 'Invalid task input JSON.'); + exit(1); +} + +$userCode = $input['code'] ?? ''; +$imports = $input['imports'] ?? []; +$GLOBALS['__DDLESS_IMPORTS_PATH__'] = $input['importsPath'] ?? null; + +if (trim($userCode) === '') { + ddless_task_emit('warn', ['message' => 'No code to execute.']); + ddless_task_done(true, $taskStartTime); + exit(0); +} + +$composerAutoload = DDLESS_PROJECT_ROOT . '/vendor/autoload.php'; +if (!file_exists($composerAutoload)) { + ddless_task_emit('error', ['message' => 'Composer autoload not found at: ' . $composerAutoload]); + ddless_task_done(false, $taskStartTime, 'Composer autoload not found.'); + exit(1); +} + +require $composerAutoload; + +$yiiFile = DDLESS_PROJECT_ROOT . '/vendor/yiisoft/yii2/Yii.php'; +if (is_file($yiiFile)) { + require_once $yiiFile; +} + +$configFile = DDLESS_PROJECT_ROOT . '/config/web.php'; +if (!is_file($configFile)) { + $configFile = DDLESS_PROJECT_ROOT . '/config/console.php'; +} + +if (!is_file($configFile)) { + ddless_task_emit('error', ['message' => 'Yii2 config not found.']); + ddless_task_done(false, $taskStartTime, 'Yii2 config not found.'); + exit(1); +} + +$config = require $configFile; + +try { + new \yii\web\Application($config); +} catch (\Throwable $e) { + ddless_task_emit('error', ['message' => 'Failed to boot Yii2: ' . $e->getMessage()]); + ddless_task_done(false, $taskStartTime, 'Failed to boot Yii2: ' . $e->getMessage()); + exit(1); +} + +class DdlessYii2TaskRunnerCommand { + private $__progressId = null; + private $__progressCurrent = 0; + private $__progressTotal = 0; + + public function component(string $name) { + return \Yii::$app->get($name); + } + + public function info($string) { ddless_task_emit('info', ['message' => (string)$string]); } + public function error($string) { ddless_task_emit('error', ['message' => (string)$string]); } + public function warn($string) { ddless_task_emit('warn', ['message' => (string)$string]); } + public function line($string) { ddless_task_emit('line', ['message' => (string)$string]); } + public function comment($string) { ddless_task_emit('comment', ['message' => (string)$string]); } + public function newLine($count = 1) { ddless_task_emit('newline', ['count' => $count]); return $this; } + public function alert($string) { ddless_task_emit('alert', ['message' => (string)$string]); } + public function listing(array $items) { ddless_task_emit('listing', ['items' => array_values(array_map('strval', $items))]); } + public function hr() { ddless_task_emit('hr', []); } + public function clipboard($text) { ddless_task_emit('clipboard', ['text' => (string)$text]); } + public function link($url, $label = '') { ddless_task_emit('link', ['url' => (string)$url, 'label' => $label ?: (string)$url]); } + + public function table($headers, $rows) { + $plainRows = []; + if ($rows instanceof \Traversable) { $rows = iterator_to_array($rows); } + foreach ($rows as $row) { + if ($row instanceof \Traversable) { $row = iterator_to_array($row); } + if (is_object($row)) { $row = (array) $row; } + $plainRows[] = array_values(array_map('strval', $row)); + } + ddless_task_emit('table', [ + 'headers' => array_values(array_map('strval', (array)$headers)), + 'rows' => $plainRows, + ]); + } + + public function json($data) { ddless_task_emit('json', ['data' => $data]); } + public function chart(): DdlessChart { return new DdlessChart(); } + + public function spin(callable $callback, string $label = 'Processing...') { + ddless_task_emit('spin', ['label' => $label, 'state' => 'start']); + try { + $result = $callback(); + ddless_task_emit('spin', ['label' => $label, 'state' => 'done']); + return $result; + } catch (\Throwable $e) { + ddless_task_emit('spin', ['label' => $label, 'state' => 'error', 'message' => $e->getMessage()]); + throw $e; + } + } + + public function time(string $label, callable $callback) { + $start = microtime(true); + $result = $callback(); + ddless_task_emit('time', ['label' => $label, 'durationMs' => round((microtime(true) - $start) * 1000, 2)]); + return $result; + } + + public function progress(int $total, string $label = ''): self { + $this->__progressId = 'prog_' . uniqid(); + $this->__progressCurrent = 0; + $this->__progressTotal = $total; + ddless_task_emit('progress', [ + 'progressId' => $this->__progressId, 'current' => 0, + 'total' => $total, 'percent' => 0, 'label' => $label, + ]); + return $this; + } + + public function advance(int $step = 1, ?string $label = null): self { + if (!$this->__progressId) return $this; + $this->__progressCurrent = min($this->__progressCurrent + $step, $this->__progressTotal); + $percent = $this->__progressTotal > 0 ? round(($this->__progressCurrent / $this->__progressTotal) * 100, 1) : 0; + ddless_task_emit('progress', [ + 'progressId' => $this->__progressId, 'current' => $this->__progressCurrent, + 'total' => $this->__progressTotal, 'percent' => $percent, 'label' => $label, + ]); + return $this; + } + + public function ask($question, $default = null, ?callable $rule = null) { + $input = ddless_prompt_with_rule('ask', (string)$question, ['promptDefault' => (string)($default ?? '')], $rule); + return $input !== '' ? $input : ($default ?? ''); + } + + public function secret($question, $fallback = true, ?callable $rule = null) { + return ddless_prompt_with_rule('secret', (string)$question, [], $rule); + } + + public function confirm($question, $default = false, ?callable $rule = null) { + $input = ddless_prompt_with_rule('confirm', (string)$question, ['promptDefault' => $default ? 'true' : 'false'], $rule); + return $input === 'true'; + } + + public function choice($question, $choices, $default = null, $attempts = null, $multiple = false, ?callable $rule = null) { + if ($choices instanceof \Traversable) { $choices = iterator_to_array($choices); } + return ddless_prompt_with_rule('choice', (string)$question, [ + 'promptOptions' => array_values((array)$choices), + 'promptDefault' => $default ?? $choices[0] ?? '', + ], $rule); + } + + public function anticipate($question, $suggestions, $default = null, ?callable $rule = null) { + if ($suggestions instanceof \Traversable) { $suggestions = iterator_to_array($suggestions); } + return ddless_prompt_with_rule('anticipate', (string)$question, [ + 'promptOptions' => array_values((array)$suggestions), + 'promptDefault' => (string)($default ?? ''), + ], $rule); + } + + public function date($question, ?string $default = null, ?callable $rule = null) { + return ddless_prompt_with_rule('date', (string)$question, ['promptDefault' => $default ?? date('Y-m-d')], $rule); + } + + public function datetime($question, ?string $default = null, ?callable $rule = null) { + return ddless_prompt_with_rule('datetime', (string)$question, ['promptDefault' => $default ?? date('Y-m-d H:i')], $rule); + } + + public function import(string $filename, string $delimiter = ',', bool $headers = true, string $encoding = 'auto'): \Generator { + $importsPath = $GLOBALS['__DDLESS_IMPORTS_PATH__'] ?? null; + if (!$importsPath) { throw new \RuntimeException("No imports directory configured."); } + $filePath = rtrim($importsPath, '/\\') . DIRECTORY_SEPARATOR . basename($filename); + if (!is_file($filePath)) { throw new \RuntimeException("Import file not found: {$filename}"); } + $handle = fopen($filePath, 'r'); + if ($handle === false) { throw new \RuntimeException("Failed to open import file: {$filename}"); } + try { + $encodingDetected = null; + if ($encoding === 'auto') { + $sample = fread($handle, 8192); + if ($sample === false || $sample === '') { return; } + if (str_starts_with($sample, "\xEF\xBB\xBF")) { $sample = substr($sample, 3); $encodingDetected = 'UTF-8'; } + else { $encodingDetected = mb_detect_encoding($sample, ['UTF-8', 'Windows-1252', 'ISO-8859-1'], true) ?: 'UTF-8'; } + rewind($handle); + $bom = fread($handle, 3); + if ($bom !== "\xEF\xBB\xBF") { rewind($handle); } + } else { $encodingDetected = $encoding; } + $needsConversion = $encodingDetected !== null && $encodingDetected !== 'UTF-8' && $encodingDetected !== 'utf-8'; + $headerRow = null; + if ($headers) { + $firstLine = fgetcsv($handle, 0, $delimiter); + if ($firstLine === false || $firstLine === null) { return; } + if ($needsConversion) { $firstLine = array_map(fn($v) => mb_convert_encoding((string)$v, 'UTF-8', $encodingDetected), $firstLine); } + $headerRow = $firstLine; + } + while (($row = fgetcsv($handle, 0, $delimiter)) !== false) { + if ($row === null) continue; + if ($needsConversion) { $row = array_map(fn($v) => mb_convert_encoding((string)$v, 'UTF-8', $encodingDetected), $row); } + if ($headerRow !== null) { $row = array_pad($row, count($headerRow), ''); yield array_combine($headerRow, array_slice($row, 0, count($headerRow))); } + else { yield $row; } + } + } finally { fclose($handle); } + } + + public function export(string $filename, callable $callback, string $delimiter = ';'): void + { + if (!str_ends_with(strtolower($filename), '.csv')) { $filename .= '.csv'; } + $page = 0; $totalRows = 0; $headersSent = false; $exportId = 'exp_' . uniqid(); + ddless_task_emit('export_begin', ['exportId' => $exportId, 'filename' => $filename]); + try { + while (true) { + $rows = $callback($page); + if ($rows === null || $rows === [] || $rows === false) { break; } + if (!is_array($rows)) { throw new \RuntimeException('Export callback must return an array of rows, null, or an empty array.'); } + $csvLines = []; + if (!$headersSent) { + $firstRow = reset($rows); + if (is_array($firstRow) || is_object($firstRow)) { $csvLines[] = ddless_csv_encode_row(array_keys((array)$firstRow), $delimiter); } + $headersSent = true; + } + foreach ($rows as $row) { $csvLines[] = ddless_csv_encode_row(array_values((array)$row), $delimiter); $totalRows++; } + ddless_task_emit('export_chunk', ['exportId' => $exportId, 'csv' => implode("\n", $csvLines) . "\n", 'totalRows' => $totalRows]); + $page++; + } + ddless_task_emit('export_end', ['exportId' => $exportId, 'filename' => $filename, 'totalRows' => $totalRows]); + } catch (\Throwable $e) { + ddless_task_emit('export_error', ['exportId' => $exportId, 'error' => $e->getMessage(), 'totalRows' => $totalRows]); + throw $e; + } + } +} + +try { + $command = new DdlessYii2TaskRunnerCommand(); + + $useStatements = ''; + foreach ($imports as $import) { + $import = trim($import); + if ($import !== '') { $useStatements .= "use {$import};\n"; } + } + + $cleanedCode = ddless_clean_php_code($userCode); + $evalCode = $useStatements . "\n" . $cleanedCode; + + $closure = \Closure::bind(function () use ($evalCode) { + return eval($evalCode); + }, $command, DdlessYii2TaskRunnerCommand::class); + + $closure(); + ddless_task_done(true, $taskStartTime); +} catch (\Throwable $e) { + $isEvalError = str_contains($e->getFile(), "eval()'d code"); + + if ($isEvalError) { + ddless_task_emit('error', ['message' => $e->getMessage()]); + ddless_task_done(false, $taskStartTime, $e->getMessage()); + exit(1); + } + + $exceptionData = [ + 'exceptionClass' => get_class($e), + 'message' => $e->getMessage(), + 'exceptionCode' => $e->getCode(), + 'exceptionFile' => $e->getFile(), + 'exceptionLine' => $e->getLine(), + ]; + + ddless_task_emit('exception', $exceptionData); + ddless_task_done(true, $taskStartTime); +}