Autarky is a minimalist PHP framework for self-sufficient programmers. It doesn't hold your hand and doesn't put restrictions on you, and is aimed at experienced developers and quick learners. It's designed for the perfect balance between flexibility, enabling rapid development, and clean object oriented programming.

Heavily inspired by Python's Flask, Autarky is a stripped down implementation of all the best features of various PHP projects. I hope you'll find that it's less invasive, more loosely coupled and less bloated than other frameworks, but is still intuitive and makes it easy to get started on new projects quickly.

Autarky is currently at version 0.x, which means breaking changes can happen as according to SemVer's 4th rule.

Goals and vision

The main goal of the framework is to provide a thin, unopiniated routing and dependency injection layer that wraps around Symfony's HttpFoundation component. Anything that goes on beyond the routing is entirely up to the programmer - you simply map URLs to classes and methods, the configurable dependency injecting container resolves these classes and calls the methods, and the return value is turned into a HTTP response.

The framework does not come with a database layer, mail library, cache layer, authentication service, queue services, translation service and so on. The implementation and integration of these are entirely left up to you, whether you want to write them yourself or pick a third party library, all you should need is a service provider class that binds the services to the container and you're good to go.

Key features and design concepts include:

Creating a project

This repository contains the framework's core classes and tests. To create a new project using Autarky, we need to use Composer:

composer create-project autarky/skeleton -s dev --prefer-dist /path/to/project

This will set up a minimalist project for you to build on top of. You can show the demo page in your browser by setting up PHP's built-in web server and open "localhost:8000" in your favourite browser:

php -S localhost:8000 -t /path/to/project/web

Keep in mind that during version 0.x, breaking changes may happen as the minor version increments. The Github releases page contains upgrade instructions and changelogs.


Autarky is hosted on Github. The main repository is autarky/framework - please use this repository for reporting issues. See the repository/repositories CONTRIBUTING file for more information.


Feel free to open an issue on Github if you have any problems or suggestions. Alternatively (or additionally), you may want to ask on Stack Overflow using the "autarky" tag.

Contact Andreas Lutro personally for potential security issues.


The contents of this repository is released under the MIT license.



Install a new project

Using either a local or global installation of composer:

$ composer create-project autarky/skeleton -s dev --prefer-dist /path/to/project

This will set up a sample project for you and install all dependencies.

It is recommended to have a look at app/start.php and read the comments to see what's going on "under the hood".

If you configure your webserver to have a docroot of the web directory, or fire up the PHP internal webserver:

$ php -S localhost -t /path/to/project/web

... you should be able to go to "localhost" in your browser and see the extremely simple welcome page.

Read some of the example files located in the skeleton project and you should be able to figure out a lot just based on that.

If you create a lot of new projects or just want to type less, you can add the following to ~/.bash_aliases in any unix-like environment:

function autarky-new {
    composer create-project autarky/skeleton -s dev --prefer-dist $@

Now you can do autarky-new /path/to/project.

Project structure

The default project structure looks like this:

├── app
│   ├── config
│   │   └── testing
│   └── templates
├── bin
├── src
│   └── Web
├── tests
├── var
│   ├── logs
│   ├── session
│   ├── twig
│   └── yaml
└── web
    └── assets

The app directory is entirely voluntary. It contains config files, templates and a couple of procedural scripts. It's meant for any code or data related to your app that isn't PHP classes. You can create as many new directories inside this directory as you want, or you can move the config and templates directory to the myproject root instead if you like.

The bin directory contains executable scripts, named after the UNIX /bin directory. This directory and its contents can also be removed if you don't want them.

The src directory contains only PHP classes. It's configured as a PSR-4 autoloaded directory in the default composer.json. Again, you're free to rename or remove this directory if you want.

The tests directory is meant for PHPUnit tests. The default phpunit.xml file has this directory configured as the application test suite. This directory, too, can be renamed or removed.

The var directory contains various cache or state data. The logs directory obviously contains application log files, the storage directory contains serialized session data if you're using the file session driver, the twig directory is for Twig's generated template files, and yaml contains cached data of parsed YAML files. If you don't use logging, file-based sessions, Twig templating or YAML config files, the var directory can safely be removed.

Finally, the web directory is meant to be the docroot of your webserver. It contains an index.php which serves as a front controller to your web application. Keeping Javascript and CSS files in an assets directory is a good practice, but entirely optional.

If you want to rename or move any of these directories, make sure to update the app/config/path.php file. If you want to move the config directory, you should also update app/start.php. The ideal would be to have the directory of the config files placed in the path.php config file - but the config loader obviously can't load the "path" config before it knows where the config files are.

In the end, you don't actually need any of these files to run a simple Autarky application. If you want to, you can easily create a single-PHP-file application using Autarky by creating an index.php that looks like this:

require_once __DIR__.'/vendor/autoload.php';

$app = new Autarky\Application('production', [
    new Autarky\Container\ContainerProvider,
    new Autarky\Routing\RoutingProvider,

$app->route('GET', '/', function() {
    return 'Hello world!';


This shows how easy it is to adjust the framework to your likings, with as much or little scaffolding and bootstrapping as you feel is optimal for your application.

Trimming dependencies

The skeleton project comes with a few third-party dependencies that are not necessary for the framework to function, but have been included as a convenience.

autarky/twig-templating provides Twig integration into the framework. If you want to use a different templating engine or no templating altogether, you can remove this.

monolog/monolog provides logging features. If you want to use a different logger or don't want logging at all, you can remove this.

symfony/browser-kit enables the BrowserKit client in PHPUnit WebTestCases. If you don't plan to use this feature, you can remove this.

symfony/yaml enables YAML config files. If you convert all your config files to PHP, you can remove this.

vlucas/dotenv makes it easy to store environment-specific variables outside of your codebase. If you don't want to do this, you can remove the package.

Configuring a webserver

You'll most likely want to put your website behind a webserver. The following are bare minimums of what you need to make an Autarky project work with common web servers. These are merely examples and should probably not be used in production without additions. Read up on the configuration options available in your web server.

Apache 2

The smallest possible virtualhost config you can create looks like this:

<VirtualHost *:80>
    DocumentRoot /path/to/autarky-project/web

To get pretty URLs, you need to add the following configuration either inside a <Location /> block in the virtualhost, or in a .htaccess file located in the web directory.

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]


The following example assumes you have PHP-FPM set up and running.

server {
    root /path/to/autarky-project/web;

    location / {
        try_files $uri /index.php$is_args$args;

    location /index.php {
        include fastcgi_params;
        fastcgi_pass; # or whatever your FCGI socket is
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;


Configuration data

Autarky comes with a cascading environment-based config data system that makes it easy to manage a global and environment-specific configurations.

First of all, take a look at the closure that is $env in the default skeleton app/start.php file. The return value of the closure passed in here should be a string, and this will be the name of the environment the application runs under. This can be strings such as 'production', 'staging', 'local' and so on.

The config store loads files from the app/config directory by default. Each file represents a top-level dictionary key - for example, $config->get('foo') will attempt to read from app/config/foo.*.

The config arrays are merged using a simple array_merge, so only top-level array keys can be overriden.

Packages can "mount" their own config directory or file(s) onto the config store. For example, a package may mount their config files as the name "foo". These files can then be accessed via $config->get('foo/bar'). This will first look for the file(s) foo.* in the package's config directory and load that. Then, if the file(s) app/config/foo/bar.* exists, those will be loaded and merged, allowing you to easily overwrite package config.

Out of the box, Autarky supports PHP array-based config files as well as YML/YAML files. You can have PHP and YML files in the same directory and they will both load fine. You usually want to use PHP files where you need some sort of programming capabilities (like re-using variables, grabbing environment variables, or finding the root path of the project using PHP constants like __DIR__), whereas YAML is well suited for static data.

Note that the "path" config file currently cannot be YML.

To use the config store, either retrieve it with $app->getConfig() or add it as a type-hinted constructor argument:

use Autarky\Config\ConfigInterface;

class MyClass {
    public function __construct(ConfigInterface $config)

Environment configuration

In some cases, you may not want to keep certain configuration data in app/config. Sensitive data such as API keys, database passwords, password salts are best kept separate from the rest of the config data.

This is best achieved by using the .env file in your project's root directory. This file is added to the project's .gitignore by default, and by utilizing the vlucas/phpdotenv package, we can load the values into the process's environment variables.

To use an environment variable in PHP, simply do getenv('ENV_VAR_KEY'). The getenv() function will return false if the environment variable does not exist, or the value of the variable if it does exist.

Runtime configuration

In addition to the config data available in the app/config directory, there is another type of configuration which we will call runtime configuration.

There are many ways to do runtime configuration of your application, including passing a callback to $app->config() either in app/start.php or a service provider, but the easiest is to create a configurator.

  1. Create a class that implements the interface Autarky\ConfiguratorInterface.
  2. Define the classes the configurator needs to configure as type-hinted constructor arguments.
  3. Execute the configuration in the configure() method.
  4. Add the name of the class to the configurators array in config/app.

Autarky comes with a couple of configurators you can look at for reference. The route configurator is very simple and a good starting point, while the log configurator is more intricate.

Note that you can add configurators via code by calling $app->config('NameOfConfiguratorClass'). This can be useful if you need to load a configurator only in specific environments, for example.


Autarky comes with a very simplistic router out of the box, based on top of nikic's FastRoute. Much of the syntax and rules are the same, especially those relating to route parameters.

Routes can be added in two ways - either programatically somewhere that won't be invoked until the application is booted (like a configurator class, or a config callback):

$router->addRoute('GET', '/', ['MyController', 'exampleAction'], 'first.route');

Or by adding them to the routes.yml config file:

  method:  'GET'
  path:    '/bar/{v1}'
  handler: ['MyController', 'otherAction']

The method you map the route to in your controller should accept the route parameters as arguments.

Keep in mind that the route parameter names have to match exactly - {foo} means the controller method must take a $foo argument.

This is not true the other way around - your controller method can take an argument that isn't a route parameter, as long as the argument has a default value.

In addition to route paramters, your controller methods can type-hint any argument to the Symfony\Component\HttpFoundation\Request class to receive the request instance. This can be useful if you need to access the request headers, query strings or something else.


The router has a few useful events you can add listeners to: The "match", "before" and "after" events.

"Before" and "after" event listeners are the most common ones. These have to be given a name and manually added to a route:

$config = [
    'before' => ['before_name'],
    'after'  => ['after_name']
$router->group($config, function($router) {
    $router->addRoute(/* ... */);

Or in your routes.yml config:

  method:  'GET'
  path:    '/foo/bar/baz'
  handler: ['MyController', 'myAction']
  before:  ['before_action']

The "before" event is fired before a route's controller/handler is invoked. In this event listener it is possible to stop the router from invoking the controller at all by setting a response, or replacing the controller.

The event object gives you access to the route object, controller, and request object via the methods getRoute(), getController(), and getRequest(). You can set a response or the controller via the setResponse() and setController() methods.

$router->addBeforeHook('before_name', function($event) {
    if (something()) {
        $event->setResponse(new Response('replacing the original response'));
    } elseif (somethingelse()) {
        $event->setController(['SomeOtherController', 'someAction']);

The "after" event is fired after a route's controller has been invoked, and can be used to manipulate or replace the response object.

The event object gives you the access to the route and request as in "before" listeners, but also the respone object via getResponse() and setResponse().

$router->addAfterHook('after_name', function($event) {
    $response = $event->getResponse();
    if (something()) {
        $response->headers->set('foo', 'bar');
    } else if (somethingelse()) {
        $event->setResponse(new Response('replacing the original response'));

There are also global before/after listeners, which don't need a name and are run on every route.

$router->addGlobalBeforeHook(function($event) {});
$router->addGlobalAfterHook(function($event) {});

The "match" event is the least useful one - it is fired when a route is matched with an URL and can be used for manipulation of the route object if you really know what you're doing.

Dependency injection

Dependency injection is one of the core values of the framework. It is one of the main tools in languages such as PHP to write pure, clean OOP, and in combination with auto-injection via reflection on typehinted dependencies, allows for rapid development with little need for configuration.

The container

At the core of any dependency injection system lies some sort of a container. These can have many names - IoC container, service container, service locator - which one of these the Autarky one is we'll leave to the pedants to find out, we'll simply call it "the container".

The container is an intelligent class capable of figuring out how to construct or locate dependencies to any class you may have in your application.

If you have a main class that has a dependency that is another class, if you type-hint against the dependency class in the main class's constructor, the dependency will automatically be resolved when you ask the container to provide an instance of the main class.

This resolving works recursively, so you can have deep hierarchies of cascading classes, and by using shared instances you can have the same instance of a particular class shared across the entire application.

As a basic example of how you can use the container's powers to your advantage: Controllers (classes mapped to routes) in Autarky are always resolved from the container, which means that any type-hinted argument in the controller's constructor will be automatically resolved. If the dependency has dependencies, the same process is repeated for those.

class MyController extends Controller {
    protected $dependency;
    public function __construct(MyDependency $dependency) {
        $this->dependency = $dependency;

This functionality is not just limited to controllers. Almost any time you pass the name of a class as a string to something inside the framework, it will be resolved from the container. This includes, but is not limited to:

The new keyword

The new keyword is the enemy of dependency injection and you should almost never use it for anything other than simple data objects (entities, Symfony HTTP response/cookie/etc objects, exceptions...).

Any time you use the new keyword to instantiate another class, keep in mind that you're making the part of your code where new is called entirely untestable, you're making it hard to replace/mock/stub the class you're instantiating, and you're preventing the class you're instantiating from having its dependencies auto-resolved.

You're also making it near impossible to have shared instances of a class in your application - for example, you don't want to open more than 1 database connection, so you'd like to have a shared PDO instance between all your application classes, but without dependency injection you will have to do new PDO in several places, or rely on nasty global/static variables.

Consider the following example:

class MyService {
    public function __construct() {
        $this->pdo = new PDO('some dsn');
        $this->validator = new MyValidator();

class MyController extends Controller {
    public function __construct() {
        $this->service = new MyService();
    public function someAction() {
        // do things with $this->service

This can easily be rewritten to use dependency injection, with the added bonus of being unit testable:

class MyService {
    public function __construct(PDO $pdo, MyValidator $validator) {
        $this->pdo = $pdo;
        $this->validator = $validator;

class MyController extends Controller {
    public function __construct(MyService $service) {
        $this->service = $service;

Configuring dependency injection

The above example will work fine if MyDependency has no dependencies, or all its dependencies can be automatically resolved without configuration. However, if your constructors type-hint an interface, abstract class or non-class parameter, you will need to configure the IoC container.

The most common configuration is to define a factory for a class, so that each time a specific class is asked for via the container, that factory is called. This is done via the define() method.

$container->define('MyNamespace\MyClass', function($container) {
    return new MyClass(/* ... */);
$container->define('MyNamespace\MyClass', ['MyClassFactory', 'makeMyClass']);

If you have a class that you want to act like a singleton, in that it will only be constructed once and the same instance will be re-used across your entire application, use the share($class) method. This can be used with or without a factory as shown above.


Sometimes you want to type-hint against an interface in your constructor parameters and let the container decide which implementation to use. This is done via the alias($original, $alias) method. This method can be used to swap different implementations as well, it does not need to be restricted to interfaces or abstract classes.

$container->alias('MyNamespace\MyImplementation', 'MyNamespace\MyInterface');

The most useful one is $container->params($class, array $params), which let you configure what classes/variables are passed to a specific class's constructor method.

$container->params('MyNamespace\MySpecificClass', [
    'MyNamespace\MyInterface' => 'MyNamespace\OtherImplementation',
    '$nonClassArgument' => 'foobar',

Factory definitions (new in 0.7)

In your application, some classes will have multiple instances that you need to keep track of. A good example of this may be PDO objects. You may want to use different PDO connections for different things. Autarky's container makes this easy out of the box - assuming you have the connections configured in app/config/database, you can do the following:

$container->resolve('MyClass', [
    'PDO' => $container->getFactory('PDO', ['$connection' => 'custom']),

Under the hood, this still utilises reflection. It does not matter which position the PDO argument is in MyClass's constructor - in fact, the PDO argument could be dropped from the constructor altogether and you could still resolve it like this without anything breaking.

The reason this works is there is a ConnectionManager class with a makePdo method that takes a $connection argument. The ConnectionManager stores any number of PDO instances, and will return the one corresponding to the connection name passed to makePdo. The makePdo method is then registered as the factory for the PDO class, so that any time you try to resolve the PDO class from the container, this factory method is called. Here's a simplified code snippet of how it's done:

$container->define('ConnectionManager', function() {
    return new ConnectionManager(/* ... */);
$container->define('PDO', ['ConnectionManager', 'makePdo']);

Avoiding reflection (new in 0.7)

If you define a factory that isn't a closure, by default, reflection is going to be used to look up the function/method and its parameters, so that the correct arguments can be resolved and passed to the callable by the container.

Reflection can cause some overhead, or may not be suitable in every situation. It is possible to bypass it entirely by creating factory objects manually.

$factory = $container->makeFactory('MyClass');
$factory = $container->makeFactory(['MyClassFactory', 'makeObject']);
$factory = $container->makeFactory(function(/* ... */) {});

Once you have a factory object, you can add arguments to it.

$factory->addClassArgument('$object', 'OtherClass', $required);
$factory->addScalarArgument('$scalar', 'string', $required, $default);

Once you've added all your arguments, add it to the container, and you can now resolve your class out of it, which will call the factory.

$container->define('MyClass', $factory);


Autarky comes with an integrated implementation of Twig, the most mature and fully-featured PHP templating library at the moment.

Twig integration is a separate package since version 0.8. composer require autarky/twig-templating if you don't already have it installed.

The templating engine can be accessed via the class Autarky\TwigTemplating\TemplatingEngine, but the easiest way is to use the render($template, array $data) method which comes with the base controller class.

Simply do return $this->render('path/to/view.twig', [..]) from a controller, and the view's contents will be rendered and put into a HTTP response with status code 200.

By default the framework comes with a Twig environment loading view files from your server's filesystem. Put a file called hello.twig in app/templates and you can do $this->render('hello.twig'). The path to the template is relative to the templates directory.

Twig functions

In addition to Twig's built-in functionality, a few Autarky-specific functions are added.

url('', [param1, param2, ...]) will return the URL to the given route with the given parameters.

asset('path/to/asset.css.js') will return the URL to the asset relative to the public directory.

partial(['MyPartialController', 'myMethod']) will resolve MyPartialController from the container, call myMethod() and print the returned value as a string.

Twig globals

flash is a global variable, being an array of flash messages passed from a controller via the $this->flashMessages($messageOrMessages) method. What this array contains is entirely up to the user of the framework - it can be simple strings or simple data objects, depending on what you need/want.


You can listen for events and modify template data at run-time.

use Autarky\TwigTemplating\Event\CreatingTemplateEvent;
use Autarky\TwigTemplating\Event\RenderingTemplateEvent;

$engine = $app->resolve('Autarky\TwigTemplating\TemplatingEngine');
$engine->creating('path/to/template.twig', ['MyListener', 'creating']);
$engine->rendering('path/to/template.twig', ['MyListener', 'rendering']);

class MyListener
    public function creating(CreatingTemplateEvent $event)
        $context = $event->getTemplate()->getContext();
        $context->foo = 'bar';
    public function rendering(RenderingTemplateEvent $event) {}

The type-hinting for the events is optional. You can also get the context by doing $event->getContext(). After setting $context->foo = 'bar', the variable foo is now available in the Twig template.

Error handling

By default, once the framework has been booted, all uncaught exceptions and PHP errors, warnings, notices etc. will be caught by the framework's error handler.

The error handler is highly configurable and makes it easy to define a global behaviour for exceptions of different types.

How it works

The error handler (Autarky\Errors\ErrorHandlerManager) keeps a doubly linked list of handlers. When an exception is thrown anywhere in your application and isn't caught by a try/catch block, it's passed to the the manager.

When the manager is passed an exception, it iterates through its list of handlers. It invokes each handler in order and checks the return value. If the handler returns null, the manager moves on to the next handler. If the handler returns anything other than null, the manager tries converting this value into a HTTP response and returns it to the end user - no further handlers are called.

Typically your handlers will be split into two types - the ones returning null and the ones returning a response. An example of handlers that return null would be a handler that writes information about the exception to a log file. An example of handlers taht return a value would be a handler that renders a pretty page telling the user what went wrong and what they can do about it.

When the manager reaches the end of the list, if no handler has returned a non-null value, the default error handler is invoked. This handler is Symfony's error handler, which renders a grey page saying "Whoops, something went wrong". If debug mode is enabled in the config/app config file, you also get information about the exception that was thrown.

If you install the package filp/whoops, the default error handler will use Whoops instead of Symfony's error handler as the default (but only in debug mode).

Adding error handlers

The easiest and recommended way to add your own error handlers is via the app.error_handlers array in the config/app file. The handler classes listed here are added to the manager's list in the order you specify them.

The classes must implement the Autarky\Errors\ErrorHandlerInterface. This interface contains a single method:

handle(Exception $exception) - handles the exception and returns either a response-like value or null.

You can also add handlers programmatically. Since the handler list is doubly linked, you can either insert handlers to the beginning (top) of the list, corresponding with high priority, or the end (bottom), corresponding with low priority.

$manager = $app->getErrorHandler();
$manager->prependHandler($myHandler); // high priority handler
$manager->appendHandler($myHandler); // low priority handler


Autarky comes with a very small set of classes to manage PDO instances. The manager will read your config/database config file and translate those into lazily loaded PDO objects. You can retrieve a connection either via the ConnectionManager:

$manager = $container->resolve('Autarky\Database\ConnectionManager');
$manager->getPdo(); // get the default connection

The default connection is defined by the connection key in the config file.

Each connection must correspond to a key in the connections array in the config file. The structure of these connection arrays vary between different PDO drivers. The only one that always has to be present is the driver key, which should be the name of a PDO driver.

There is also the optional pdo_options array, which contain additional PDO options, some of which are described here.

Lastly there is the optional pdo_init_commands array, which should be an array of strings containing SQL commands that are run right after the connection has been extablished.

In addition, any array keys not specifically mentioned here will simply be appended to the DSN. You can look up available DSN key/value pairs on the PDO driver page on For example, with Postgres, if you want to set the charset when connection, your PDO DSN should look like this:

new PDO("host=localhost;options='--client_encoding=UTF8'", $user, $pass);

To emulate this with the connection config, simply add the following to a Postgres connection array:

'options' => "'--client_encoding=UTF8'",


SQLite only requires a path key, which should be the full path to the SQLite database file.

Postgres (pgsql)

You can specify dbname, username, password, host and port.

MSSQL (sqlsrv)

Database, username and password should be specified. If you need to specify a port, simply add a comma behind the Server value:

'Server' => 'localhost,1521'


You can specify host, port, dbname, username, password and charset.

Other drivers

Other drivers may or may not work. Try to pass an array of all the DSN key/value pairs as specified by the documentation.

Console (CLI)

Autarky ships with the Symfony 2 Console component for making CLI interfaces to your application. The console application is invoked by running php app/console.php, which will show a list of available commands.

If you have an existing Command class, you can add it by adding a line to your app/console.php file:

$console->add(new MyNamespace\MyCommand(/* ... */));

If your command has dependencies that should be resolved from the container, you can do the following:

$container = $app->getContainer();
$command = $container->resolve('MyNamespace\MyCommand');

If you want your commands to lazily resolve dependencies for performance reasons, it's best to pass the $container instance to the command and then call $container->resolve() inside the command's fire() method.

Service providers can configure the console application via their bootConsole(ConsoleApplication $app) method:

use Symfony\Component\Console\Application as ConsoleApplication;
public function bootConsole(ConsoleApplication $app) {}


Autarky provides a pretty bare minimum of what is needed to get you going with PHP development. You, the user, are meant to provide most of the meat for it.

Adding functionality

As an example, let's say you want to use Eloquent, the Laravel ORM, in your application. The recommended course of action here is to write your own service provider, in which you share an instance of the class Illuminate\Database\Capsule\Manager. An official example on how to use this class can be found here.

All we need to do is bind the manager class as a shared instance onto the container inside the service provider's register() method, like this:

use Illuminate\Database\Capsule\Manager;
use Autarky\Providers\AbstractProvider;

class EloquentProvider extends AbstractProvider {
    public function register() {
        $container = $this->app->getContainer();

        $container->define('Illuminate\Database\Capsule\Manager', function() {
            $capsule = new Capsule;
            return $capsule;


If we want Eloquent to be available we also need to run a special method. We do this by adding a config() callback to the register() method block:

$this->app->config(function($app) {

Replacing functionality

You might also want to replace core components of the framework. Most of them are registered by service providers, so you can simply remove the service provider from your app/start.php file and replace it with your own.

In some cases, the core classes implement an interface in order to decouple with other parts of the framework. If the class you want to replace implements an interface, you too should implement this interface and alias your class as the default implementation of this interface in the container.