TryCatch
This library provides a fluent interface for implementing the try-catch-finally pattern in PHP with strong type inference support.
Type Inference
The TryCatch library uses PHP's generic type system to provide better type inference and IDE autocompletion. When you use the TryCatch::with() method, the return type of your try block is automatically inferred and propagated to the result of the execute() method.
This means your IDE can provide proper autocompletion for the result of execute() based on what your try block returns.
<?php
// The IDE knows that $result is a string
$result = TryCatch::with(fn () => "Hello world", $logger)->execute();
$result->length; // IDE shows error: string doesn't have a length property
// The IDE knows that $user is a User object
$user = TryCatch::with(fn () => User::create("email@example.com", "John"), $logger)->execute();
$user->getName(); // IDE provides autocompletion for User methods
Basic Usage
<?php
use Atournayre\TryCatch\TryCatch;
use Atournayre\TryCatch\ThrowableHandlerCollection;
use Psr\Log\LoggerInterface;
// Create a new TryCatch instance
// The return type of the try block (string) is inferred and propagated to $result
$result = TryCatch::with(
tryBlock: function(): string {
// Your code that might throw exceptions
return 'Success';
},
logger: $logger
)
->catch(\RuntimeException::class, function(\RuntimeException $exception): string {
// Handle RuntimeException
return 'Handled RuntimeException';
})
->catch(\InvalidArgumentException::class, function(\InvalidArgumentException $exception): string {
// Handle InvalidArgumentException
return 'Handled InvalidArgumentException';
})
->finally(function() {
// Code that will always run
// If this block returns a non-null value, it will be used as the result of execute()
return 'Value from finally block'; // Optional: override the result
})
->execute();
// The IDE knows that $result is a string and provides autocompletion
$uppercase = strtoupper($result); // IDE provides autocompletion for string functions
TryCatch
The main class that implements the try-catch-finally pattern.
with<TReturn>(tryBlock: \Closure(): TReturn, logger: LoggerInterface): self<TReturn>catch(throwableClass: string, handler: \Closure): self<T>- Preserves the generic type Tfinally(finallyBlock: \Closure): self<T>- Preserves the generic type T. If the finally block returns a non-null value, it will be used as the result of execute()reThrow(throwableClass: string, message: string = '', code: int = 0): self<T>- Logs and rethrows the caught exception wrapped in a ThrowableInterfaceexecute(): T- Returns the type specified by the generic parameter T
ThrowableHandlerCollection
Collection of throwable handlers.
asList([collection: array = []]): selfadd(value: ThrowableHandlerInterface): selfaddWithCallback(value: mixed, callback: \Closure): selfremove(keys: int|array|\Traversable): selffindHandlerFor(throwable: \Throwable): ThrowableHandlerInterface<mixed>- Returns a handler that can handle the throwable
ThrowableHandler
Implementation for throwable handlers.
new(throwableClass: string, handlerFunction: \Closure): self- Creates a new handler for the specified throwable classcanHandle(throwable: \Throwable): bool- Checks if the handler can handle the given throwablehandle(throwable: \Throwable): T- Handles the throwable and returns a value of type T
ThrowableHandlerInterface
Interface for throwable handlers.
@template T- Generic type parameter for the return type of the handlercanHandle(throwable: \Throwable): bool- Checks if the handler can handle the given throwablehandle(throwable: \Throwable): T- Handles the throwable and returns a value of type T
Advanced Usage
Using Multiple Exception Handlers
<?php
use Atournayre\TryCatch\TryCatch;
use Psr\Log\LoggerInterface;
// Create a TryCatch instance with multiple catch handlers
// The return type of the try block (string) is inferred and propagated to $result
$result = TryCatch::with(
tryBlock: function(): string {
// Your code that might throw exceptions
return 'Success';
},
logger: $logger
)
->catch(\RuntimeException::class, function(\RuntimeException $exception): string {
return 'Handled RuntimeException';
})
->catch(\InvalidArgumentException::class, function(\InvalidArgumentException $exception): string {
return 'Handled InvalidArgumentException';
})
->finally(function() {
// Code that will always run
// If this block returns a non-null value, it will be used as the result of execute()
return 'Value from finally block'; // Optional: override the result
})
->execute();
// The IDE knows that $result is a string and provides autocompletion
$length = strlen($result); // IDE provides autocompletion for string functions
Real-world Example: User Creation
This example demonstrates how to use TryCatch in a real application context to handle user creation with proper exception handling:
<?php
use Atournayre\Tests\Fixtures\Exception\InvalidEmailException;
use Atournayre\Tests\Fixtures\User;
use Atournayre\Contracts\TryCatch\ExecutableTryCatchInterface;
use Atournayre\TryCatch\TryCatch;
use Psr\Log\LoggerInterface;
/**
* @implements ExecutableTryCatchInterface<User|null>
*/
final readonly class CreateUserTryCatch implements ExecutableTryCatchInterface
{
private function __construct(
private string $email,
private string $name,
private LoggerInterface $logger,
)
{
}
public static function new(
string $email,
string $name,
LoggerInterface $logger,
): self
{
return new self(
email: $email,
name: $name,
logger: $logger,
);
}
/**
* {@inheritdoc}
* @throws \Throwable
*/
public function execute(): ?User
{
return TryCatch::with(
tryBlock: function (): User {
return User::create($this->email, $this->name);
},
logger: $this->logger,
)->catch(
throwableClass: InvalidEmailException::class,
handler: function (InvalidEmailException $exception): ?User {
// Log the exception or perform other actions
// Return a default user or null
return null;
},
)->execute();
}
}
This example shows:
- Creating a dedicated class that implements ExecutableTryCatchInterface
- Using the generic type parameter <User|null> to specify the return type
- Using TryCatch to handle a specific exception type (InvalidEmailException)
- Returning null when an exception occurs
- Using the fluent interface to create a clean, readable implementation
- Providing proper type information for IDE autocompletion
Void Try Block with Catch Returning a Value
In some cases, you might have a try block that doesn't return anything (void), but you want the catch handler to return a value when an exception occurs. The TryCatch class handles this case automatically:
<?php
use Atournayre\TryCatch\TryCatch;
use Psr\Log\LoggerInterface;
// The try block performs an action but doesn't return anything
$result = TryCatch::with(
tryBlock: function(): void {
// Your code that might throw exceptions but doesn't return anything
performSomeAction();
},
logger: $logger
)
->catch(\RuntimeException::class, function(\RuntimeException $exception): string {
// When an exception occurs, the catch handler can return a value
return 'Error occurred: ' . $exception->getMessage();
})
->execute();
// If an exception was caught, $result will contain the value returned by the catch handler
// If no exception was caught, $result will be null
This is useful for scenarios where a process doesn't normally return anything, but you want to return information about errors when they occur.
Using reThrow to Wrap Exceptions
The reThrow() method allows you to catch any exception thrown in the try block, log it, and then rethrow it wrapped in a new exception of a specified class. This is useful for converting low-level exceptions into domain-specific exceptions while preserving the original exception information.
<?php
use Atournayre\Common\Exception\RuntimeException;
use Atournayre\TryCatch\TryCatch;
use Psr\Log\LoggerInterface;
try {
// The try block might throw various exceptions
$result = TryCatch::with(
tryBlock: function() {
// Code that might throw exceptions
return callExternalService();
},
logger: $logger
)
->reThrow(
// Specify the exception class to use for wrapping
// This class must implement ThrowableInterface
throwableClass: RuntimeException::class,
// Optional: Provide a custom message (if empty, the original message will be used)
message: 'An error occurred while calling the external service',
// Optional: Provide a custom code (if 0, the original code will be used)
code: 500
)
->execute();
} catch (RuntimeException $exception) {
// Handle the wrapped exception
// The original exception is available as $exception->getPrevious()
$originalException = $exception->getPrevious();
// Log or handle the exception as needed
echo "Error: " . $exception->getMessage();
echo "Original error: " . $originalException->getMessage();
}
Key points about reThrow():
- It logs the original exception before rethrowing
- It wraps the original exception in a new exception of the specified class
- The original exception is preserved as the "previous" exception
- If no message is provided (or an empty string), the original exception's message is used
- If no code is provided (or 0), the original exception's code is used
- The specified throwable class must implement ThrowableInterface