diff --git a/app/Config/Format.php b/app/Config/Format.php index b79faa28192e..8de909ec9a9f 100644 --- a/app/Config/Format.php +++ b/app/Config/Format.php @@ -1,54 +1,59 @@ - + */ public $formatters = [ - 'application/json' => \CodeIgniter\Format\JSONFormatter::class, - 'application/xml' => \CodeIgniter\Format\XMLFormatter::class, - 'text/xml' => \CodeIgniter\Format\XMLFormatter::class, + 'application/json' => 'CodeIgniter\Format\JSONFormatter', + 'application/xml' => 'CodeIgniter\Format\XMLFormatter', + 'text/xml' => 'CodeIgniter\Format\XMLFormatter', ]; - /* - |-------------------------------------------------------------------------- - | Formatters Options - |-------------------------------------------------------------------------- - | - | Additional Options to adjust default formatters behaviour. - | For each mime type, list the additional options that should be used. - | - */ + /** + * -------------------------------------------------------------------------- + * Formatters Options + * -------------------------------------------------------------------------- + * + * Additional Options to adjust default formatters behaviour. + * For each mime type, list the additional options that should be used. + * + * @var array + */ public $formatterOptions = [ 'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, 'application/xml' => 0, @@ -63,24 +68,11 @@ class Format extends BaseConfig * @param string $mime * * @return \CodeIgniter\Format\FormatterInterface + * + * @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead. */ public function getFormatter(string $mime) { - if (! array_key_exists($mime, $this->formatters)) - { - throw new \InvalidArgumentException('No Formatter defined for mime type: ' . $mime); - } - - $class = $this->formatters[$mime]; - - if (! class_exists($class)) - { - throw new \BadMethodCallException($class . ' is not a valid Formatter.'); - } - - return new $class(); + return Services::format()->getFormatter($mime); } - - //-------------------------------------------------------------------- - } diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index f42e5346a504..daee90ca45fd 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -40,7 +40,7 @@ namespace CodeIgniter\API; use CodeIgniter\HTTP\Response; -use Config\Format; +use Config\Services; /** * Response trait. @@ -102,6 +102,13 @@ trait ResponseTrait */ protected $format = 'json'; + /** + * Current Formatter instance. This is usually set by ResponseTrait::format + * + * @var \CodeIgniter\Format\FormatterInterface + */ + protected $formatter; + //-------------------------------------------------------------------- /** @@ -123,7 +130,8 @@ public function respond($data = null, int $status = null, string $message = '') // Create the output var here in case of $this->response([]); $output = null; - } // If data is null but status provided, keep the output empty. + } + // If data is null but status provided, keep the output empty. elseif ($data === null && is_numeric($status)) { $output = null; @@ -134,8 +142,7 @@ public function respond($data = null, int $status = null, string $message = '') $output = $this->format($data); } - return $this->response->setBody($output) - ->setStatusCode($status, $message); + return $this->response->setBody($output)->setStatusCode($status, $message); } //-------------------------------------------------------------------- @@ -159,7 +166,7 @@ public function fail($messages, int $status = 400, string $code = null, string $ $response = [ 'status' => $status, - 'error' => $code === null ? $status : $code, + 'error' => $code ?? $status, 'messages' => $messages, ]; @@ -386,25 +393,25 @@ protected function format($data = null) return $data; } - $config = new Format(); - $format = "application/$this->format"; + $format = Services::format(); + $mime = "application/{$this->format}"; // Determine correct response type through content negotiation if not explicitly declared - if (empty($this->format) || ! in_array($this->format, ['json', 'xml'])) + if (empty($this->format) || ! in_array($this->format, ['json', 'xml'], true)) { - $format = $this->request->negotiate('media', $config->supportedResponseFormats, false); + $mime = $this->request->negotiate('media', $format->getConfig()->supportedResponseFormats, false); } - $this->response->setContentType($format); + $this->response->setContentType($mime); // if we don't have a formatter, make one if (! isset($this->formatter)) { // if no formatter, use the default - $this->formatter = $config->getFormatter($format); // @phpstan-ignore-line + $this->formatter = $format->getFormatter($mime); } - if ($format !== 'application/json') + if ($mime !== 'application/json') { // Recursively convert objects into associative arrays // Conversion not required for JSONFormatter diff --git a/system/Config/Services.php b/system/Config/Services.php index 615739d68bc9..8da343c7edfd 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -51,6 +51,7 @@ use CodeIgniter\Encryption\EncrypterInterface; use CodeIgniter\Encryption\Encryption; use CodeIgniter\Filters\Filters; +use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\CURLRequest; @@ -83,6 +84,7 @@ use Config\Email as EmailConfig; use Config\Encryption as EncryptionConfig; use Config\Exceptions as ExceptionsConfig; +use Config\Format as FormatConfig; use Config\Filters as FiltersConfig; use Config\Honeypot as HoneypotConfig; use Config\Images; @@ -311,6 +313,28 @@ public static function filters(FiltersConfig $config = null, bool $getShared = t //-------------------------------------------------------------------- + /** + * The Format class is a convenient place to create Formatters. + * + * @param \Config\Format|null $config + * @param boolean $getShared + * + * @return \CodeIgniter\Format\Format + */ + public static function format(FormatConfig $config = null, bool $getShared = true) + { + if ($getShared) + { + return static::getSharedInstance('format', $config); + } + + $config = $config ?? config('Format'); + + return new Format($config); + } + + //-------------------------------------------------------------------- + /** * The Honeypot provides a secret input on forms that bots should NOT * fill in, providing an additional safeguard when accepting user input. diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index 4529fd0fafa0..abaf3d1456a6 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -1,16 +1,95 @@ -config = $config; + } + + /** + * Returns the current configuration instance. + * + * @return \Config\Format + */ + public function getConfig() + { + return $this->config; + } + + /** + * A Factory method to return the appropriate formatter for the given mime type. + * + * @param string $mime + * + * @throws \CodeIgniter\Format\Exceptions\FormatException + * + * @return \CodeIgniter\Format\FormatterInterface + */ + public function getFormatter(string $mime): FormatterInterface + { + if (! array_key_exists($mime, $this->config->formatters)) + { + throw FormatException::forInvalidMime($mime); + } + + $className = $this->config->formatters[$mime]; + + if (! class_exists($className)) + { + throw FormatException::forInvalidFormatter($className); + } + + $class = new $className(); + + if (! $class instanceof FormatterInterface) + { + throw FormatException::forInvalidFormatter($className); + } + + return $class; + } +} diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 383649f8b5ea..05e918a11a2d 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -41,7 +41,9 @@ use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Pager\PagerInterface; -use Config\Format; +use Config\Services; +use DateTime; +use DateTimeZone; /** * Representation of an outgoing, getServer-side response. @@ -376,9 +378,9 @@ public function getReason(): string * * @return Response */ - public function setDate(\DateTime $date) + public function setDate(DateTime $date) { - $date->setTimezone(new \DateTimeZone('UTC')); + $date->setTimezone(new DateTimeZone('UTC')); $this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT'); @@ -479,13 +481,7 @@ public function getJSON() if ($this->bodyFormat !== 'json') { - /** - * @var Format $config - */ - $config = config(Format::class); - $formatter = $config->getFormatter('application/json'); - - $body = $formatter->format($body); + $body = Services::format()->getFormatter('application/json')->format($body); } return $body ?: null; @@ -521,13 +517,7 @@ public function getXML() if ($this->bodyFormat !== 'xml') { - /** - * @var Format $config - */ - $config = config(Format::class); - $formatter = $config->getFormatter('application/xml'); - - $body = $formatter->format($body); + $body = Services::format()->getFormatter('application/xml')->format($body); } return $body; @@ -554,13 +544,7 @@ protected function formatBody($body, string $format) // Nothing much to do for a string... if (! is_string($body) || $format === 'json-unencoded') { - /** - * @var Format $config - */ - $config = config(Format::class); - $formatter = $config->getFormatter($mime); - - $body = $formatter->format($body); + $body = Services::format()->getFormatter($mime)->format($body); } return $body; diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php index c0070c8534b5..e1a3406c994e 100644 --- a/system/Language/en/Format.php +++ b/system/Language/en/Format.php @@ -15,6 +15,8 @@ */ return [ + 'invalidFormatter' => '"{0}" is not a valid Formatter class.', 'invalidJSON' => 'Failed to parse json string, error: "{0}".', + 'invalidMime' => 'No Formatter defined for mime type: "{0}".', 'missingExtension' => 'The SimpleXML extension is required to format XML.', ]; diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index d13218cabb1c..efcd3f828e65 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -1,4 +1,5 @@ format = $format; } } - } diff --git a/system/Test/FeatureResponse.php b/system/Test/FeatureResponse.php index a99a9dd5dd73..88414ecce1cf 100644 --- a/system/Test/FeatureResponse.php +++ b/system/Test/FeatureResponse.php @@ -1,4 +1,5 @@ getFormatter('application/json'); - $test = $formatter->format($test); + $test = Services::format()->getFormatter('application/json')->format($test); } $this->assertJsonStringEqualsJsonString($test, $json, 'Response does not contain matching JSON.'); diff --git a/system/Test/Mock/MockResourcePresenter.php b/system/Test/Mock/MockResourcePresenter.php index 5358feacf499..cc70d302cb3f 100644 --- a/system/Test/Mock/MockResourcePresenter.php +++ b/system/Test/Mock/MockResourcePresenter.php @@ -1,10 +1,13 @@ -format; } - } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index c8503ec2ff5e..472f7ed3a208 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -1,9 +1,12 @@ assertInstanceOf(\CodeIgniter\Filters\Filters::class, $result); } + public function testFormat() + { + $this->assertInstanceOf(Format::class, Services::format()); + } + + public function testUnsharedFormat() + { + $this->assertInstanceOf(Format::class, Services::format(null, false)); + } + public function testHoneypot() { $result = Services::honeypot(); diff --git a/tests/system/Format/FormatTest.php b/tests/system/Format/FormatTest.php new file mode 100644 index 000000000000..da46ebc5e771 --- /dev/null +++ b/tests/system/Format/FormatTest.php @@ -0,0 +1,64 @@ +format = new Format(new \Config\Format()); + } + + public function testFormatConfigType() + { + $config = new \Config\Format(); + $format = new Format($config); + + $this->assertInstanceOf('Config\Format', $format->getConfig()); + $this->assertSame($config, $format->getConfig()); + } + + public function testGetFormatter() + { + $this->assertInstanceof(FormatterInterface::class, $this->format->getFormatter('application/json')); + } + + public function testGetFormatterExpectsExceptionOnUndefinedMime() + { + $this->expectException(FormatException::class); + $this->expectExceptionMessage('No Formatter defined for mime type: "application/x-httpd-php".'); + $this->format->getFormatter('application/x-httpd-php'); + } + + public function testGetFormatterExpectsExceptionOnUndefinedClass() + { + $this->format->getConfig()->formatters = array_merge( + $this->format->getConfig()->formatters, + ['text/xml' => 'App\Foo'] + ); + + $this->expectException(FormatException::class); + $this->expectExceptionMessage('"App\Foo" is not a valid Formatter class.'); + $this->format->getFormatter('text/xml'); + } + + public function testGetFormatterExpectsExceptionOnClassNotImplementingFormatterInterface() + { + $this->format->getConfig()->formatters = array_merge( + $this->format->getConfig()->formatters, + ['text/xml' => 'CodeIgniter\HTTP\URI'] + ); + + $this->expectException(FormatException::class); + $this->expectExceptionMessage('"CodeIgniter\HTTP\URI" is not a valid Formatter class.'); + $this->format->getFormatter('text/xml'); + } +} diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 0d2d8ed5bfc6..3eba93b1fc91 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -1,11 +1,12 @@ setLink($pager); $this->assertEquals( - '; rel="first",; rel="prev",; rel="next",; rel="last"', $response->getHeader('Link')->getValue() + '; rel="first",; rel="prev",; rel="next",; rel="last"', + $response->getHeader('Link')->getValue() ); $pager->store('default', 1, 10, 200); $response->setLink($pager); $this->assertEquals( - '; rel="next",; rel="last"', $response->getHeader('Link')->getValue() + '; rel="next",; rel="last"', + $response->getHeader('Link')->getValue() ); $pager->store('default', 20, 10, 200); $response->setLink($pager); $this->assertEquals( - '; rel="first",; rel="prev"', $response->getHeader('Link')->getValue() + '; rel="first",; rel="prev"', + $response->getHeader('Link')->getValue() ); } @@ -340,10 +344,6 @@ public function testSetCookieSuccessOnPrefix() public function testJSONWithArray() { - $response = new Response(new App()); - $config = new Format(); - $formatter = $config->getFormatter('application/json'); - $body = [ 'foo' => 'bar', 'bar' => [ @@ -352,8 +352,9 @@ public function testJSONWithArray() 3, ], ]; - $expected = $formatter->format($body); + $expected = Services::format()->getFormatter('application/json')->format($body); + $response = new Response(new App()); $response->setJSON($body); $this->assertEquals($expected, $response->getJSON()); @@ -362,10 +363,6 @@ public function testJSONWithArray() public function testJSONGetFromNormalBody() { - $response = new Response(new App()); - $config = new Format(); - $formatter = $config->getFormatter('application/json'); - $body = [ 'foo' => 'bar', 'bar' => [ @@ -374,8 +371,9 @@ public function testJSONGetFromNormalBody() 3, ], ]; - $expected = $formatter->format($body); + $expected = Services::format()->getFormatter('application/json')->format($body); + $response = new Response(new App()); $response->setBody($body); $this->assertEquals($expected, $response->getJSON()); @@ -385,10 +383,6 @@ public function testJSONGetFromNormalBody() public function testXMLWithArray() { - $response = new Response(new App()); - $config = new Format(); - $formatter = $config->getFormatter('application/xml'); - $body = [ 'foo' => 'bar', 'bar' => [ @@ -397,8 +391,9 @@ public function testXMLWithArray() 3, ], ]; - $expected = $formatter->format($body); + $expected = Services::format()->getFormatter('application/xml')->format($body); + $response = new Response(new App()); $response->setXML($body); $this->assertEquals($expected, $response->getXML()); @@ -407,10 +402,6 @@ public function testXMLWithArray() public function testXMLGetFromNormalBody() { - $response = new Response(new App()); - $config = new Format(); - $formatter = $config->getFormatter('application/xml'); - $body = [ 'foo' => 'bar', 'bar' => [ @@ -419,8 +410,9 @@ public function testXMLGetFromNormalBody() 3, ], ]; - $expected = $formatter->format($body); + $expected = Services::format()->getFormatter('application/xml')->format($body); + $response = new Response(new App()); $response->setBody($body); $this->assertEquals($expected, $response->getXML()); @@ -520,6 +512,7 @@ public function testTemporaryRedirectGet11() } //-------------------------------------------------------------------- + // Make sure cookies are set by RedirectResponse this way // See https://github.com/codeigniter4/CodeIgniter4/issues/1393 public function testRedirectResponseCookies() @@ -536,6 +529,7 @@ public function testRedirectResponseCookies() } //-------------------------------------------------------------------- + // Make sure we don't blow up if pretending to send headers public function testPretendOutput() { @@ -559,8 +553,6 @@ public function testInvalidSameSiteCookie() $this->expectException(HTTPException::class); $this->expectExceptionMessage(lang('HTTP.invalidSameSiteSetting', ['Invalid'])); - - $response = new Response($config); + new Response($config); } - } diff --git a/tests/system/Test/FeatureResponseTest.php b/tests/system/Test/FeatureResponseTest.php index 75e6aa9ae66a..35e56548baa0 100644 --- a/tests/system/Test/FeatureResponseTest.php +++ b/tests/system/Test/FeatureResponseTest.php @@ -2,11 +2,13 @@ use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Response; +use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\FeatureResponse; +use Config\App; +use Config\Services; -class FeatureResponseTest extends \CodeIgniter\Test\CIUnitTestCase +class FeatureResponseTest extends CIUnitTestCase { - /** * @var FeatureResponse */ @@ -115,7 +117,7 @@ public function testAssertRedirectFail() public function testAssertRedirectSuccess() { $this->getFeatureResponse('

Hello World

'); - $this->feature->response = new RedirectResponse(new Config\App()); + $this->feature->response = new RedirectResponse(new App()); $this->assertTrue($this->feature->response instanceof RedirectResponse); $this->assertTrue($this->feature->isRedirect()); @@ -125,7 +127,7 @@ public function testAssertRedirectSuccess() public function testGetRedirectUrlReturnsUrl() { $this->getFeatureResponse('

Hello World

'); - $this->feature->response = new RedirectResponse(new Config\App()); + $this->feature->response = new RedirectResponse(new App()); $this->feature->response->redirect('foo/bar'); $this->assertEquals('foo/bar', $this->feature->getRedirectUrl()); @@ -221,8 +223,7 @@ public function testAssertCookieExpired() public function testGetJSON() { $this->getFeatureResponse(['foo' => 'bar']); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/json'); + $formatter = Services::format()->getFormatter('application/json'); $this->assertEquals($formatter->format(['foo' => 'bar']), $this->feature->getJSON()); } @@ -231,8 +232,6 @@ public function testEmptyJSON() { $this->getFeatureResponse('

Hello World

'); $this->response->setJSON('', true); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/json'); // this should be "" - json_encode(''); $this->assertEquals('""', $this->feature->getJSON()); @@ -242,8 +241,6 @@ public function testFalseJSON() { $this->getFeatureResponse('

Hello World

'); $this->response->setJSON(false, true); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/json'); // this should be FALSE - json_encode(false) $this->assertEquals('false', $this->feature->getJSON()); @@ -253,8 +250,6 @@ public function testTrueJSON() { $this->getFeatureResponse('

Hello World

'); $this->response->setJSON(true, true); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/json'); // this should be TRUE - json_encode(true) $this->assertEquals('true', $this->feature->getJSON()); @@ -273,8 +268,7 @@ public function testInvalidJSON() public function testGetXML() { $this->getFeatureResponse(['foo' => 'bar']); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/xml'); + $formatter = Services::format()->getFormatter('application/xml'); $this->assertEquals($formatter->format(['foo' => 'bar']), $this->feature->getXML()); } @@ -315,41 +309,31 @@ public function testJsonExactString() ]; $this->getFeatureResponse($data); + $formatter = Services::format()->getFormatter('application/json'); - $config = new \Config\Format(); - $formatter = $config->getFormatter('application/json'); - $expected = $formatter->format($data); - - $this->feature->assertJSONExact($expected); + $this->feature->assertJSONExact($formatter->format($data)); } protected function getFeatureResponse($body = null, array $responseOptions = [], array $headers = []) { - $this->response = new Response(new \Config\App()); + $this->response = new Response(new App()); $this->response->setBody($body); - if (count($responseOptions)) + foreach ($responseOptions as $key => $value) { - foreach ($responseOptions as $key => $value) - { - $method = 'set' . ucfirst($key); + $method = 'set' . ucfirst($key); - if (method_exists($this->response, $method)) - { - $this->response = $this->response->$method($value); - } + if (method_exists($this->response, $method)) + { + $this->response = $this->response->$method($value); } } - if (count($headers)) + foreach ($headers as $key => $value) { - foreach ($headers as $key => $value) - { - $this->response = $this->response->setHeader($key, $value); - } + $this->response = $this->response->setHeader($key, $value); } $this->feature = new FeatureResponse($this->response); } - } diff --git a/user_guide_src/source/changelogs/v4.0.5.rst b/user_guide_src/source/changelogs/v4.0.5.rst index 4b0df7bcdb7b..4342a1914efd 100644 --- a/user_guide_src/source/changelogs/v4.0.5.rst +++ b/user_guide_src/source/changelogs/v4.0.5.rst @@ -21,3 +21,4 @@ Bugs Fixed: Deprecations: - Deprecated ``BaseCommand::getPad`` in favor of ``BaseCommand::setPad``. +- Deprecated ``Config\Format::getFormatter`` in favor of ``CodeIgniter\Format\Format::getFormatter``