M README.md => README.md +5 -0
@@ 1,6 1,7 @@
# Thirdplace Components
This is a collection of components. Mostly for my own personal use.
+Mostly used in web applications.
## Tutorial
@@ 8,6 9,10 @@ Install:
composer require thirdplace/components
+Run unit tests:
+
+ ./vendor/bin/tunit
+
## Explanation
## How-to
M composer.json => composer.json +2 -6
@@ 29,13 29,9 @@
"files": [
"./src/common.php",
"./src/Clock.php",
- "./src/Container.php",
- "./src/CsrfMiddleware.php",
- "./src/http/Header.php",
- "./src/http/Response.php",
- "./src/Url.php",
"./src/Renderer.php",
- "./src/Shell.php"
+ "./src/http/Response.php",
+ "./src/Url.php"
]
},
"bin": [
M src/Container.php => src/Container.php +3 -5
@@ 3,8 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-final class ContainerException extends Exception {}
-
final class Container implements \ArrayAccess
{
private array $values = [];
@@ 13,7 11,7 @@ final class Container implements \ArrayAccess
public function offsetSet($offset, $value)
{
if (isset($this->values[$offset])) {
- throw new ContainerException(sprintf('Key already exists "%s"', $offset));
+ throw new \Exception(sprintf('Key already exists "%s"', $offset));
}
if (! $value instanceof \Closure) {
@@ 26,7 24,7 @@ final class Container implements \ArrayAccess
public function offsetGet($offset)
{
if (!isset($this->values[$offset])) {
- throw new ContainerException(sprintf('Unknown key: "%s"', $offset));
+ throw new \Exception(sprintf('Unknown key: "%s"', $offset));
}
if (isset($this->resolved[$offset])) {
@@ 43,6 41,6 @@ final class Container implements \ArrayAccess
public function offsetUnset($offset)
{
- throw new ContainerException('unset() not implemented');
+ throw new \Exception('unset() not implemented');
}
}
M src/CsrfMiddleware.php => src/CsrfMiddleware.php +4 -6
@@ 3,8 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-final class CsrfException extends Exception {}
-
final class CsrfMiddleware
{
public function __invoke(Request $request): Request
@@ 13,20 11,20 @@ final class CsrfMiddleware
$tokenFromRequest = $request->post('csrf', [], true);
if (!$tokenFromRequest) {
- throw new CsrfException('Missing token from request');
+ throw new \Exception('Missing token from request');
}
if (!isset($tokenFromRequest['key'])) {
- throw new CsrfException('Missing token key from request');
+ throw new \Exception('Missing token key from request');
}
$tokenFromSession = $_SESSION['thirdplace'][$tokenFromRequest['key']] ?? null;
if (!$tokenFromSession) {
- throw new CsrfException('Unknown token key');
+ throw new \Exception('Unknown token key');
}
if (! hash_equals($tokenFromRequest['value'], $tokenFromSession['value'])) {
- throw new CsrfException('Token mismatch');
+ throw new \Exception('Token mismatch');
}
}
M src/Renderer.php => src/Renderer.php +55 -16
@@ 3,28 3,19 @@ declare(strict_types=1);
namespace Thirdplace;
-final class RendererException extends Exception {}
-
final class Renderer
{
- private const CONFIG = [
- /**
- * Templates folder
- */
- 'templates' => './',
-
- /**
- * Default context that is always available in templates
- */
- 'context' => [],
- ];
-
private array $config;
private array $context;
public function __construct(array $config = [])
{
- $this->config = array_merge(self::CONFIG, $config);
+ $defaultConfig = [
+ 'templates' => './',
+ // Default context that is always available in templates
+ 'context' => [],
+ ];
+ $this->config = array_merge($defaultConfig, $config);
$this->context = $this->config['context'];
}
@@ 58,6 49,54 @@ final class Renderer
return $candidate;
}
- throw new RendererException(sprintf('Unable to resolve file path: "%s"', $filePath));
+ throw new \Exception(sprintf('Unable to resolve file path: "%s"', $filePath));
+ }
+}
+
+/**
+ * Escape for html context
+ */
+function e(string $s): string
+{
+ return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
+}
+
+/**
+ * Explicitly don't escape
+ */
+function raw(string $s): string
+{
+ return $s;
+}
+
+/**
+ * Sanitize for html tag context. Don't use for html tag attribute context!
+ */
+function sanitize(string $s): string
+{
+ return filter_var($s, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
+}
+
+/**
+ * Truncate string
+ */
+function truncate(string $s, int $length = 100, $marker = ' [...]'): string
+{
+ $s = trim($s);
+
+ if (mb_strlen($s) < $length) {
+ return $s;
}
+
+ $naiveTruncate = mb_substr($s, 0, $length);
+
+ $lastSpace = mb_strrpos($naiveTruncate, ' ');
+
+ if ($lastSpace === false) {
+ $lastSpace = $length;
+ }
+
+ $properTruncate = mb_substr($s, 0, $lastSpace);
+
+ return $properTruncate . $marker;
}=
\ No newline at end of file
M src/Shell.php => src/Shell.php +2 -4
@@ 3,8 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-final class ShellException extends Exception {}
-
final class Shell
{
public static function execute(string $command, array $arguments = []): array
@@ 20,9 18,9 @@ final class Shell
case 0:
return $result;
case 127:
- throw new ShellException(sprintf('Command not found: "%s"', $command));
+ throw new \Exception(sprintf('Command not found: "%s"', $command));
default:
- throw new ShellException(sprintf('Unsuccessful: "%s"', $command));
+ throw new \Exception(sprintf('Unsuccessful: "%s"', $command));
}
}
}
M src/Url.php => src/Url.php +2 -3
@@ 3,8 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-final class UrlException extends Exception {}
-
function url(string $url): Url
{
return Url::fromString($url);
@@ 26,7 24,7 @@ final class Url implements \JsonSerializable
$url = trim($url);
if (!self::validate($url)) {
- throw new UrlException(sprintf('Not valid: "%s"', $url));
+ throw new \Exception(sprintf('Not valid: "%s"', $url));
}
return (new self)
@@ 175,6 173,7 @@ final class Url implements \JsonSerializable
$path = rtrim($path, '/') . '/';
}
+ // todo: remove trailing / if the path, query and fragment is empty
return sprintf(
'%s://%s%s%s%s%s',
$this->scheme,
M src/common.php => src/common.php +1 -50
@@ 3,54 3,5 @@ declare(strict_types=1);
namespace Thirdplace;
-class Exception extends \Exception {}
+final class HttpException extends \Exception {}
-final class HttpException extends Exception {}
-
-/**
- * Escape for html context
- */
-function e(string $s): string
-{
- return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
-}
-
-/**
- * Explicitly don't escape
- */
-function raw(string $s): string
-{
- return $s;
-}
-
-/**
- * Sanitize for html tag context. Don't use for html tag attribute context!
- */
-function sanitize(string $s): string
-{
- return filter_var($s, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
-}
-
-/**
- * Truncate string
- */
-function truncate(string $s, int $length = 100, $marker = ' [...]'): string
-{
- $s = trim($s);
-
- if (mb_strlen($s) < $length) {
- return $s;
- }
-
- $naiveTruncate = mb_substr($s, 0, $length);
-
- $lastSpace = mb_strrpos($naiveTruncate, ' ');
-
- if ($lastSpace === false) {
- $lastSpace = $length;
- }
-
- $properTruncate = mb_substr($s, 0, $lastSpace);
-
- return $properTruncate . $marker;
-}>
\ No newline at end of file
M src/http/Cookie.php => src/http/Cookie.php +4 -1
@@ 24,6 24,9 @@ final class Cookie
public function send(): void
{
- setcookie($this->name, $this->value);
+ $options = [
+
+ ];
+ setcookie($this->name, $this->value, $options);
}
}=
\ No newline at end of file
M src/http/CurlHttpClient.php => src/http/CurlHttpClient.php +10 -3
@@ 13,6 13,7 @@ final class CurlHttpClient implements HttpClient
'max_redirs' => 5,
'headers' => [],
'body' => null,
+ 'max_file_size' => 5 * 1024**2 // 5MiB
];
private $ch;
@@ 39,6 40,7 @@ final class CurlHttpClient implements HttpClient
curl_setopt($this->ch, CURLOPT_TIMEOUT, $config['timeout']);
curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, $config['follow_location']);
curl_setopt($this->ch, CURLOPT_MAXREDIRS, $config['max_redirs']);
+ curl_setopt($this->ch, CURLOPT_MAXFILESIZE, $config['max_file_size']);
curl_setopt($this->ch, CURLOPT_ENCODING, '');
if ($method === 'GET') {
@@ 60,12 62,12 @@ final class CurlHttpClient implements HttpClient
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $requestHeaders);
$responseHeaders = [];
-
curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($ch, $rawHeader) use (&$responseHeaders) {
$len = strlen($rawHeader);
if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader) || $rawHeader==="\r\n") {
return $len;
}
+ // todo: maybe try..catch here
$header = Header::fromString($rawHeader);
if (!isset($responseHeaders[$header->name])) {
@@ 79,12 81,17 @@ final class CurlHttpClient implements HttpClient
$body = curl_exec($this->ch);
if ($body === false) {
- throw new HttpException($url . ': ' . curl_error($this->ch), curl_errno($this->ch));
+ throw new HttpException(sprintf('%s: %s', $url, curl_error($this->ch)), curl_errno($this->ch));
}
$cookies = array_map(fn($cookie) => Cookie::fromString($cookie), $responseHeaders['set-cookie'] ?? []);
- return new Response($body, curl_getinfo($this->ch, CURLINFO_RESPONSE_CODE), $responseHeaders, $cookies);
+ return new Response(
+ $body,
+ curl_getinfo($this->ch, CURLINFO_RESPONSE_CODE),
+ $responseHeaders,
+ $cookies
+ );
}
public function __destruct()
M => +1 -3
@@ 3,8 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
final class HeaderException extends Exception {}
final class Header
{
public const APPLICATION_ATOM_XML = 'application/atom+xml';
@@ 27,7 25,7 @@ final class Header
$header = explode(':', $rawHeader);
if (count($header) === 1) {
throw new HeaderException(sprintf('Invalid header string: "%s"', $rawHeader));
throw new \Exception(sprintf('Invalid header string: "%s"', $rawHeader));
}
$name = mb_strtolower(trim($header[0]));
M src/http/Response.php => src/http/Response.php +1 -0
@@ 15,6 15,7 @@ function text(string $body, int $code = 200): Response
function json(array $body, int $code = 200): Response
{
+ // todo: remove dep
$json = Json::encode($body, JSON_PRETTY_PRINT);
return response($json, $code)->withHeader('content-type', 'application/json');
}
M src/logger/StreamHandler.php => src/logger/StreamHandler.php +3 -1
@@ 31,7 31,9 @@ final class StreamHandler implements Handler
if ($record['context'] === []) {
$record['context'] = '';
} else {
- $record['context'] = Json::encode($record['context'], JSON_PRETTY_PRINT) ?: '["Unable to json encode context"]';
+ // todo: remove dep
+ $json = Json::encode($record['context'], JSON_PRETTY_PRINT) ?: '["Unable to json encode context"]';
+ $record['context'] = $json;
}
$result = sprintf(
M test/container.php => test/container.php +3 -1
@@ 15,4 15,6 @@ assertSame(2, $sut['two']);
$sut['test'] = fn($c) => $c['two'];
assertSame(2, $sut['test']);
-expectException(fn() => $sut['foo'] = 'bar');>
\ No newline at end of file
+expectException(function() use ($sut) {
+ $sut['foo'] = 'bar';
+});<
\ No newline at end of file