Castor Labs
Castor Labs mission is to help PHP developers write maintainable, robust and efficient PHP applications.
We do this by actively:
- Pushing for what we consider to be best practices in the PHP community
- Proposing and implementing standards for the PHP community
- Publishing packages with useful abstractions for PHP projects
We differ from the PHP FIG in one big crucial point: our goal is not interoperability, but rather, to make our packages the de-facto building blocks in which you can build your PHP applications.
We are not a closed organization, and our libraries and standards are open for revision and contribution by anyone in the PHP Open Source community.
Every package is independent and has their own maintainer(s). We don't have a coordinated release cycle like Symfony or other vendors do. Instead, we rely on semantic versioning, and empowering maintainers to push the changes they consider necessary to the packages they maintain.
Check out some of the packages we have, or you can also read some of our standards.
Castor Context
Context passing abstraction for modern PHP projects, inspired in Golang's context
package.
Introduction
Context is an abstraction for passing request-scoped values down the call stack of an application.
The public api is inspired in Golang's context
package.
Installation
composer require castor/context
Quick Start
<?php
use Castor\Context;
$ctx = Context\nil(); // This is a default base context
$ctx = Context\withValue($ctx, 'foo', 'bar'); // This returns a new context with the passed values stored
// Later in the call stack
echo $ctx->value('foo'); // Prints: bar
Why this Library?
This library attempts to solve the so called context-passing problem. For a good overview of the problem you can read Matthias Noback's blog post about it, or continue reading here.
Context Passing
In traditional PHP applications, the context in which an action is being executed is a fundamental piece of information to determine program logic. By context, we could mean, for example, the user that is executing the action, or the current tenant.
If you use the popular PHP frameworks, you can get that information pretty easily. It usually involves calling a global (like the Auth facade in Laravel) or injecting a service that stores this information inside some shared class state (like Symfony's Security service).
The problem with this solution is that it relies on global shared state. Although their access mechanisms vary (one uses global access while the other uses dependency injection), they still rely on storing state in a shared instance. They are fundamentally the same thing.
A better approach is to create a context object and pass it down explicitly down the call stack, so interested parties can extract from it the information they need to process the request. But this object needs to be carefully designed, so it does not expose a wide api and pollute client code making the interface too big (a violation of the Dependency Inversion principle).
The context package provides such object. It allows you to give every request its own immutable context, where you can store request specific values. It is not a service, so it can't be shared globally, and it must be passed explicitly down the call stack, so no magic mutations happen during its lifecycle.
The Context
interface
This package defines one very small and simple interface called Castor\Context
. Your code should typehint to this
interface always. This library is intentionally simple and has a small api surface and footprint because it is
intended to be used even in your domain layer.
As we said, the Context
interface has an intentionally simple api, with just one method: value(mixed $key): mixed
.
This method returns the value "stored" in the context for the given key. Pay special attention to the fact that
you can store values of any type as keys, and comparison of the keys is done by strict equality.
If you are a good observer, you'll quickly realize the Context
interface does not define any methods that mutate
its internal state like set(mixed $key, mixed $value): void
. Although we considered that method for a second, we
copied from Go the idea to make this interface unavoidably immutable. You can only "store" new values by composing
a new Context
, and we have provided some functions to ease that process.
The Context\nil()
and the Context\withValue()
functions
First, in order to "store" a value in the context, you need to get the fallback context. This is some sort of "empty"
context that always returns null
. You call Context\nil()
to do this.
<?php
use Castor\Context;
// This gives you a context that always returns null for any key
$ctx = Context\nil();
var_dump($ctx->value('foo')); // Prints: NULL
Once you have a Context
instance, you can "store" a value in it by calling Context\withValue()
:
<?php
use Castor\Context;
// This gives you a context that always returns null for any key
$ctx = Context\nil();
// This returns a new context with the stored key value pair
$ctx = Context\withValue($ctx, 'foo', 'bar');
var_dump($ctx->value('foo')); // Prints: string(3) "bar"
Basically, those two functions and a single interface is the whole api surface of the context package. In the next chapter we'll go to some guides on how to leverage the power of the context api to build powerful functionality in your applications.
Guides & Examples
Learn how to work with the Context
api by following these guides and examples.
Implementing Multi-Tenancy
A common use case for many applications is to support multi-tenancy. Multi tenancy is the ability of an application to operate for multiple tenants or customers using the same instance and codebase.
Now we are going to see an example of how we can implement multi tenancy using the context api.
Let's suppose our imaginary application determines the tenant based on the subdomain, in the HTTP layer, in some middleware:
<?php
namespace MyMultiTenantApp;
use Castor\Context;
use Castor\Http\Handler;
use Castor\Http\Request;
use Castor\Http\ResponseWriter;
class TenancyMiddleware implements Handler
{
private Handler $next;
private TenantRepository $tenants;
public function __construct(Handler $next, TenantRepository $tenants)
{
$this->next = $next;
$this->tenants = $tenants;
}
public function handle(Context $ctx, ResponseWriter $wrt, Request $req): void
{
$hostname = $req->getUri()->getHostname();
// This should separate subdomain.domain.tld
$parts = explode('.', $hostname, 3);
// No subdomain
if (count($parts) <= 2) {
$this->next->handle($ctx, $wrt, $req);
return;
}
$tenantName = $parts[0] ?? '';
try {
$tenant = $this->tenants->ofId($ctx, $id);
} catch (NotFound) {
$this->next->handle($ctx, $wrt, $req);
return;
}
// Once we have the tenant, we store it in the context
$ctx = Context\withValue($ctx, 'tenant', $tenant);
// And we pass it to the next handler
$this->next->handle($ctx, $wrt, $req);
}
}
So, you have "captured" some important information about the context in which the request is being made, which is the current tenant in use (or no tenant at all if no subdomain is present).
Now, further layers of the application that are interested in this piece of information, can modify their behaviour based on the absence or presence of such value.
For instance, we want to list all the users, but only those of the current tenant:
<?php
namespace MyMultiTenantApp;
use Castor\Context;
use MyMultiTenantApp\QueryBuilder;
use MyMultiTenantApp\Tenant;
class UserRepository
{
private QueryBuilder $query;
public function __construct(QueryBuilder $query)
{
$this->query = $query;
}
public function all(Context $ctx): array
{
$query = $this->query->select()->from('users');
$tenant = $ctx->value('tenant');
if ($tenant instanceof Tenant) {
$query = $query->where('tenant_id = ?', $tenant->getId());
}
return $query->execute();
}
}
The context interface provides several benefits in this case. It's transparency means you could deploy this for a single tenant and the application would work exactly the same. You could even implement lazy database connection based on the tenant to point them to different databases.
Possibilities are endless.
Passing Logger Context
Another common problem in PHP applications is the passing of context for log calls. Say, for example, you want all your
logs to contain the request_id
property, so you can group them together and explore all the logs emitted by a single
request, but you don't want to pass the request id to every single call that could reach a logger. That would be a
terrible thing to do. What you want here is the power of context.
In this short guide, we'll explore more advanced things you can do with context. First, let's create a simple object to hold the state we want to accumulate to later send to the logger.
<?php
namespace MyApp\Logger;
class LogContext
{
protected array $data;
public function __construct()
{
$this->data = [];
}
public function add(string $key, mixed $value): LogContext
{
$this->data[$key] = $value;
return $this;
}
public function merge(array $data): LogContext
{
$this->data = array_merge($this->data, $data);
return $this;
}
public function toArray(): array
{
return $this->data;
}
}
This is the class that we are going to store in the context. It does not need to be immutable, because Context is immutable and every context instance will have its own instance of this class. There is no possibility that that instance will be shared outside the scope of the request, so it is safe. Immutability is good in certain contexts, but in this case it is not needed. We want this class to be mutable by design.
Now, we will provide a simple set of functions on top of the context functions, to ease the manipulation of this class. Note these are pure functions: passed the same input, they will yield the same output.
<?php
namespace MyApp\Logger;
use Castor\Context;
// First we create an enum for the key
enum Key
{
case LOG_CONTEXT
}
// This function add an entry to the stored LogContext
function with_log_context(Context $ctx, string $key, mixed $value): Context
{
$logCtx = $ctx->value(Key::LOG_CONTEXT);
// If we have an instance already, we mutate that, and we don't touch the context
if ($logCtx instanceof LogContext) {
$logCtx->add($key, $value);
return $ctx;
}
// We mutate the context only if we don't have a LogContext object in it
$logCtx = new LogContext();
$logCtx->add($key, $value);
return Context\withValue($ctx, Key::LOG_CONTEXT, $logCtx);
}
function get_log_context(Context $ctx): LogContext
{
// We try to not throw exceptions but provide defaults instead.
return $ctx->value(Key::LOG_CONTEXT) ?? new LogContext();
}
Note how these functions make it easy to add this particular bit of information to the context. They also hide the need for client code to know the context key, and they take sensible decisions for client code. The functions are pure, and with no side effects.
Now, let's work in the middleware that will capture our request:
<?php
namespace MyApp\Http;
use MyApp\Logger;
use Castor\Context;
use Castor\Http\Handler;
use Castor\Http\Request;
use Castor\Http\ResponseWriter;
use Ramsey\Uuid;
use function MyApp\Logger\with_log_context;
class RequestIdMiddleware implements Handler
{
private Handler $next;
public function __construct(Handler $next)
{
$this->next = $next;
}
public function handle(Context $ctx, ResponseWriter $wrt, Request $req): void
{
$requestId = req->getHeaders()->get('X-Request-Id');
// If no request id comes, we assign one
if ($requestId === '') {
$requestId = Uuid::v4();
}
$ctx = with_log_context($ctx, 'request_id', $requestId);
$this->next->handle($ctx, $wrt, $req);
}
}
Then, the only bit left to do is to consume this from the code that calls the logger.
<?php
namespace MyApp\Services;
use Castor\Context;
use Psr\Log\LoggerInterface;
use function MyApp\Logger\get_log_context;
class SomeServiceThatLogsStuff
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function doSomething(Context $ctx): void
{
// Do an action and then log it.
// The context holds request_id and other values passed by other layers.
$this->logger->debug('Action completed', get_log_context($ctx)->toArray());
}
}
With this simple approach, your log calls will be richer and easier to filter. And you didn't need to pollute your call stack with a massive class or resort to use globals. Everything is explicit, and by using the custom functions you make easy for your consumers to extract or insert things from and to the context.
This shows how evolvable the Context
abstraction is, and how you can compose functionality on top of it by
keeping the interface thin.
Best Practices
The Context
api is definitely a new pattern and approach to solve the context passing problem in PHP.
Whenever a new pattern is introduced, is always important to define some best practices for its good use.
These are some of what we consider to be those best practices.
Custom functions SHOULD be defined to work with Context
Context is all about composition, and you should also compose the base context functions to provide a nicer api to store or retrieve context values, like we did in the logger example. Of course, you could also rely on static methods, but in my personal opinion, functions are nicer.
You can even go as far as to create your own Context
implementation that composes a Context
. We do that for some
of our libraries. But we even discourage that. The thinner you can keep Context
api surface, the better.
Context
SHOULD be the first argument of the function or method
Whenever a function or method call accepts a Context
, this should be the first argument of the function, and
it should be named $ctx
. We know some people hate abbreviations and prefer more explicit names. I agree with that
suggestion almost in every situation, but in this case the abbreviation does not yield any sort of confusion and
is pretty universal and easily understood.
It also has the benefit that it is short, and since Context
is there to be added to your public api, we want
to minimize as much as possible the space impact that has in your method's argument list.
Enums SHOULD be used as keys rather than strings
When calling Context\withValue()
prefer enums as keys. They are lightweight, offer autocompletion, and they cannot
collide like strings could. In the case that your application still does not support PHP 8.1 and above, you MUST use
string keys with a vendor namespace.
Context
SHOULD NOT be stored inside other data structures
Always explicitly pass Context
. Do not store it inside objects or arrays, unless is explicitly necessary. For
instance, if you are using the Context
api in PSR-7 applications, it is very likely your Context
instance
will be stored in the Request attributes (which is implemented by an array). This is acceptable, but for a better
HTTP layer supporting context natively, we recommend castor/http
.
Context\withValue
SHOULD NOT be overused
Because of its particular implementation, every time you add a value to a Context
, you increase the potential call
stack size to reach the value by 1. Although the performance impact of this is negligent, is still slower than fetching
a value directly from a map, for instance.
So, bottom line, don't overuse Context\withValue
. This means that if you have to store related values in
Context
, store a data structure instead and not each value individually.
Again, the performance impact of not doing this is negligible, so measure and make decisions based on that.
In a potential new major version, we are considering swapping the
Context\withValue()
implementation by using aDS\Map
if the extension is available to avoid the performance penalty.
Context
SHOULD NOT contain immutable values
A Context
implementation is already immutable, and it's lifetime does not go outside the request. This means it is
safe for manipulation and free of unexpected side effects.
As long as Context
holds values derived from the request, whose lifetime will also die with it, then it is safe to
store mutable values in it. If you store immutable values and decide that a new reference of that value needs to be
passed down the call stack it means the value should have never been immutable in the first place. You'll have to call
Context\withValue
again and "override" that value.
Services SHOULD NOT be stored inside Context
We greatly discourage storing services inside Context
. It is not a good idea to mix request-scoped values with
application scoped-values like services. Always prefer dependency injection instead of passing services down the
context.
There could be some use cases when this could make sense. For instance, if you need to pass an event loop instance down the call stack to reach some method and make it work asynchronously. I would say that is part of the execution context, although an event loop can be classified as a service.
Frequently Asked Questions
1. Why no array context?
For mainly two reasons. (1) I wanted an api that used composition to extend the context, and (2) I wanted an API that let me use any type as key. Arrays only support integers and strings.
2. Should I use it in my domain?
Yes! It is designed for this very purpose. Once you add the Context
interface as a type hint of a repository
method or some other function, a world of possibilities are opened in terms of evolvavility and extensibility.
3. Will this package ever have breaking changes?
No. Context promises a stable api since is really one of the most important building blocks of our libraries.
However, we are considering expanding the api surface for a possible v2
version once we have implemented async
libraries, and we decide we need cancellation signals, similar to what Golang context api has at the minute.
Castor Uri
RFC 3986 compliant URI value object for modern PHP applications.
Introduction
Castor URI provides an RFC 3986 compliant URI value object.
Installation
composer require castor/uri
Quick Start
<?php
use Castor\Net\Uri;
$uri = Uri::parse('https://example.com/hello?foo=bar');
echo $uri->getScheme(); // Prints: https
echo $uri->getHost(); // Prints: example.com
echo $uri->getPath(); // Prints: /hello
echo $uri->getRawQuery(); // Prints: foo=bar
echo $uri->getQuery()->add('foo', 'foo')->encode(); // Prints: foo=bar&foo=foo
Why this Library?
A URI abstraction is essential in any standard library.
API Overview
How To Guide
Mutating the Query String
Accessing Raw Values
Escaping Path and Fragment
Frequently Asked Questions
1. Why no PSR-7?
Insert here long, boring explanation of why using interfaces for value objects as a standard is a bad idea.