src/JSend/JSendResponse.php
<?php
namespace JSend;
use BadMethodCallException;
use JsonSerializable;
use UnexpectedValueException;
class JSendResponse implements JsonSerializable
{
public const SUCCESS = 'success';
public const FAIL = 'fail';
public const ERROR = 'error';
public const KEY_STATUS = 'status';
public const KEY_DATA = 'data';
public const KEY_MESSAGE = 'message';
public const KEY_CODE = 'code';
protected string $status;
/** @var array<mixed>|null */
protected ?array $data;
protected ?string $errorCode;
protected ?string $errorMessage;
protected int $jsonEncodeOptions = 0;
/**
* From the spec:
* Description: All went well, and (usually) some data was returned.
* Required : data
*
* @param array<mixed>|null $data
*
* @return JSendResponse
* @throws InvalidJSendException
*/
public static function success(array $data = null): JSendResponse
{
return new static(static::SUCCESS, $data);
}
/**
* From the spec:
* Description: There was a problem with the data submitted, or some pre-condition of the API call wasn't satisfied
* Required : data
*
* @param array<mixed>|null $data
*
* @return JSendResponse
* @throws InvalidJSendException
*/
public static function fail(array $data = null): JSendResponse
{
return new static(static::FAIL, $data);
}
/**
* From the spec:
* Description: An error occurred in processing the request, i.e. an exception was thrown
* Required : errorMessage
* Optional : errorCode, data
*
* @param string $errorMessage
* @param string|null $errorCode
* @param array<mixed>|null $data
*
* @return JSendResponse
*
* @throws InvalidJSendException if empty($errorMessage) is true
*/
public static function error(string $errorMessage, string $errorCode = null, array $data = null): JSendResponse
{
return new static(static::ERROR, $data, $errorMessage, $errorCode);
}
/**
* JSendResponse constructor.
*
* @param string $status one of static::SUCCESS, static::FAIL, static::ERROR
* @param array<mixed>|null $data
* @param string|null $errorMessage mandatory for errors
* @param string|null $errorCode
*
* @throws InvalidJSendException if status is not valid or status is error and empty($errorMessage) is true
*/
final public function __construct(string $status, array $data = null, $errorMessage = null, $errorCode = null)
{
if (!$this->isStatusValid($status)) {
throw new InvalidJSendException('Status does not conform to JSend spec.');
}
$this->status = $status;
if ($status === static::ERROR) {
if (empty($errorMessage)) {
throw new InvalidJSendException('Errors must contain a message.');
}
$this->errorMessage = $errorMessage;
$this->errorCode = $errorCode;
}
$this->data = $data;
}
public function getStatus(): string
{
return $this->status;
}
/**
* @return array<mixed>|null
*/
public function getData(): ?array
{
return $this->data;
}
/**
* @return null|string
*/
public function getErrorMessage(): ?string
{
if ($this->isError()) {
return $this->errorMessage;
}
throw new BadMethodCallException('Only responses with a status of error may have an error message.');
}
/**
* @return null|string
*/
public function getErrorCode(): ?string
{
if ($this->isError()) {
return $this->errorCode;
}
throw new BadMethodCallException('Only responses with a status of error may have an error code.');
}
protected function isStatusValid(string $status): bool
{
$validStatuses = array(static::SUCCESS, static::FAIL, static::ERROR);
return \in_array($status, $validStatuses, true);
}
public function isSuccess(): bool
{
return $this->status === static::SUCCESS;
}
public function isFail(): bool
{
return $this->status === static::FAIL;
}
public function isError(): bool
{
return $this->status === static::ERROR;
}
/**
* Serializes the class into an array
* @return array<mixed> the serialized array
*/
public function asArray(): array
{
$theArray = [static::KEY_STATUS => $this->status];
if ($this->data) {
$theArray[static::KEY_DATA] = $this->data;
}
if (!$this->data && !$this->isError()) {
// Data is optional for errors, so it should not be set
// rather than be null.
$theArray[static::KEY_DATA] = null;
}
if ($this->isError()) {
$theArray[static::KEY_MESSAGE] = (string)$this->errorMessage;
if (!empty($this->errorCode)) {
$theArray[static::KEY_CODE] = (int)$this->errorCode;
}
}
return $theArray;
}
public function setEncodingOptions(int $options): void
{
$this->jsonEncodeOptions = $options;
}
/**
* Encodes the class into JSON
* @return false|string the raw JSON
*/
public function encode(): false|string
{
return json_encode($this, $this->jsonEncodeOptions);
}
/**
* Implements JsonSerializable interface
* @return array<mixed>
*/
public function jsonSerialize(): array
{
return $this->asArray();
}
/**
* @return string
*/
public function __toString()
{
$encode = $this->encode();
return $encode===false ? "" : $encode;
}
/**
* Encodes the class into JSON and sends it as a response with
* the 'application/json' header
*/
public function respond(): void
{
header('Content-Type: application/json');
echo $this->encode();
}
/**
* Takes raw JSON (JSend) and builds it into a new JSendResponse
*
* @param string $json the raw JSON (JSend) to decode
* @param int<1, max> $depth User specified recursion depth, defaults to 512
* @param int $options Bitmask of JSON decode options.
*
* @return JSendResponse if JSON is invalid
* @throws InvalidJSendException if JSend does not conform to spec
* @see json_decode()
*/
public static function decode(string $json, int $depth = 512, int $options = 0): JSendResponse
{
$rawDecode = json_decode($json, true, $depth, $options);
if ($rawDecode === null) {
throw new UnexpectedValueException('JSON is invalid.');
}
if ((!\is_array($rawDecode)) || (!array_key_exists(static::KEY_STATUS, $rawDecode))) {
throw new InvalidJSendException('JSend must be an object with a valid status.');
}
$status = $rawDecode[static::KEY_STATUS];
$data = $rawDecode[static::KEY_DATA] ?? null;
$errorMessage = $rawDecode[static::KEY_MESSAGE] ?? null;
$errorCode = $rawDecode[static::KEY_CODE] ?? null;
if ($status === static::ERROR && $errorMessage === null) {
throw new InvalidJSendException('JSend errors must contain a message.');
}
if ($status !== static::ERROR && !array_key_exists(static::KEY_DATA, $rawDecode)) {
throw new InvalidJSendException('JSend must contain data unless it is an error.');
}
return new static($status, $data, $errorMessage, $errorCode);
}
}