*/ protected array $filters = []; /** * @var array */ protected array $payload = []; /** * @var bool */ protected static bool $showSensitive = false; /** * @var array */ protected static array $models = []; /** * Response constructor. * * @param SwooleHTTPResponse $response Native response to be passed to parent constructor */ public function __construct(SwooleHTTPResponse $response) { parent::__construct($response); } /** * HTTP content types */ public const CONTENT_TYPE_YAML = 'application/x-yaml'; public const CONTENT_TYPE_NULL = 'null'; public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; /** * Register a model * * @param Model $model * @return void */ public static function setModel(Model $model): void { self::$models[$model->getType()] = $model; } /** * Get Model Object * * @param string $key * @return Model * @throws Exception */ public function getModel(string $key): Model { if (!isset(self::$models[$key])) { throw new Exception('Undefined model: ' . $key); } return self::$models[$key]; } /** * Get Models List * * @return Model[] */ public function getModels(): array { return self::$models; } /** * Check if a model exists * * @param string $key * @return bool */ public static function hasModel(string $key): bool { return isset(self::$models[$key]); } public function applyFilters(array $data, string $model): array { foreach ($this->filters as $filter) { $data = $filter->parse($data, $model); } return $data; } /** * Validate response objects and outputs * the response according to given format type * * @param Document $document * @param string $model * * return void * @throws Exception */ public function dynamic(Document $document, string $model): void { $output = $this->output(clone $document, $model); $output = $this->applyFilters($output, $model); switch ($this->getContentType()) { case self::CONTENT_TYPE_JSON: try { $this->json(!empty($output) ? $output : new \stdClass()); } catch (JsonException $e) { throw new Exception('Failed to parse response: ' . $e->getMessage(), 400); } break; case self::CONTENT_TYPE_YAML: $this->yaml(!empty($output) ? $output : new \stdClass()); break; case self::CONTENT_TYPE_NULL: break; case self::CONTENT_TYPE_MULTIPART: $this->multipart(!empty($output) ? $output : new \stdClass()); break; default: if ($model === self::MODEL_NONE) { $this->noContent(); } else { $this->json(!empty($output) ? $output : new \stdClass()); } break; } } /** * Generate valid response object from document data * * @param Document $document * @param string $model * * return array * @return array * @throws Exception */ public function output(Document $document, string $model): array { $data = clone $document; $model = $this->getModel($model); $output = []; $data = $model->filter($data); if ($model->isAny()) { $this->payload = $data->getArrayCopy(); return $this->payload; } foreach ($model->getRules() as $key => $rule) { if (!$data->isSet($key) && $rule['required']) { // do not set attribute in response if not required if (\array_key_exists('default', $rule)) { $data->setAttribute($key, $rule['default']); } else { throw new Exception('Model ' . $model->getName() . ' is missing response key: ' . $key); } } if (!$data->isSet($key) && !$rule['required']) { // set output key null if data key is not set and required is false $output[$key] = null; continue; } if ($rule['array']) { if (!\is_array($data[$key])) { throw new Exception($key . ' must be an array of type ' . $rule['type']); } foreach ($data[$key] as $index => $item) { if ($item instanceof Document) { $ruleType = null; if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { $condition = false; foreach ($this->getModel($type)->conditions as $attribute => $val) { $condition = $item->getAttribute($attribute) === $val; if (!$condition) { break; } } if ($condition) { $ruleType = $type; break; } } } else { $ruleType = $rule['type']; } if ($ruleType === null || !self::hasModel($ruleType)) { throw new Exception('Missing model for rule: ' . ($ruleType ?? 'null') . ' (key: ' . $key . ')'); } $data[$key][$index] = $this->output($item, $ruleType); } } } else { if ($data[$key] instanceof Document) { $data[$key] = $this->output($data[$key], $rule['type']); } } if ($rule['sensitive']) { $roles = $this->authorization->getRoles(); $user = $this->user ?? new DBUser(); $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) { $data->setAttribute($key, ''); } } $output[$key] = $data[$key]; } $this->payload = $output; return $this->payload; } /** * Output response * * Generate HTTP response output including the response header (+cookies) and body and prints them. * * @param string $body * * @return void */ public function file(string $body = ''): void { $this->payload = [ 'payload' => $body ]; $this->send($body); } /** * YAML * * This helper is for sending YAML HTTP response. * It sets relevant content type header ('application/x-yaml') and convert a PHP array ($data) to valid YAML using native yaml_parse * * @see https://en.wikipedia.org/wiki/YAML * * @param array $data * * @return void * @throws Exception */ public function yaml(array $data): void { if (!extension_loaded('yaml')) { throw new Exception('Missing yaml extension. Learn more at: https://www.php.net/manual/en/book.yaml.php'); } $this ->setContentType(Response::CONTENT_TYPE_YAML) ->send(\yaml_emit($data, YAML_UTF8_ENCODING)); } /** * Multipart * * This helper is for sending multipart/form-data HTTP response. * It sets relevant content type header ('multipart/form-data') and convert a PHP array ($data) to valid Multipart using BodyMultipart * * @param array $data * * @return void */ public function multipart(array $data): void { $multipart = new BodyMultipart(); foreach ($data as $key => $value) { $multipart->setPart($key, $value); } $this ->setContentType($multipart->exportHeader()) ->send($multipart->exportBody()); } /** * JSON * * This helper is for sending JSON HTTP response. * It sets relevant content type header ('application/json') and convert a PHP array ($data) to valid JSON using native json_encode * * @see http://en.wikipedia.org/wiki/JSON * * @param mixed $data * @return void */ public function json($data): void { if (!is_array($data) && !$data instanceof \stdClass) { throw new \Exception('Response body is not a valid JSON object.'); } $this ->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8) ->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); } /** * @return array */ public function getPayload(): array { return $this->payload; } /** * Function to add a response filter, the order of filters are first in - first out. * * @param $filter - the response filter to set * * @return void */ public function addFilter(Filter $filter): void { $this->filters[] = $filter; } /** * Return the currently set filters * * @return array */ public function getFilters(): array { return $this->filters; } /** * Reset filters * * @return void */ public function resetFilters(): void { $this->filters = []; } /** * Check if a filter has been set * * @return bool */ public function hasFilters(): bool { return !empty($this->filters); } /** * Static wrapper to show sensitive data in response * * @param callable(): array $callback The callback to show sensitive information for * @return array */ public static function showSensitive(callable $callback): array { try { self::$showSensitive = true; return $callback(); } finally { self::$showSensitive = false; } } private ?Authorization $authorization = null; private ?DBUser $user = null; public function setAuthorization(Authorization $authorization): void { $this->authorization = $authorization; } public function setUser(DBUser $user): void { $this->user = $user; } }