DI container with auto-wiring feature So I decided to write my own DI container for educational purposes and would like some feedback on how I can improve the quality of my code.

I'm not sure about the documentation as most of my experience has been with closed source personal code, so I would like some feedback on that.

Another area is where I am using exit(), maybe an exception would be better? Although my experience is minimal here, maybe Exception or RuntimeException?

I would also prefer responses about common good practices over personal preferences.

&lt;?php declare (strict_types = 1);

namespace Rosa;

use RuntimeException;

class Rosa {
    private $objects = [];

    /**
     * Register an instantiated object to the container.
     *
     * @param object $object
     */
    public function register(object $object) : void {
        $this-&gt;objects[get_class($object)] = $object;
    }

    /**
     * Fetch a cached object from the container.
     *
     * @param string $objectName
     * @return object
     */
    public function fetch(string $objectName) : object {
        if (array_key_exists($objectName, $this-&gt;objects)) {
            return $this-&gt;objects[$objectName];
        }

        return $this-&gt;make($objectName);
    }

    /**
     * Creates an object from its name and auto-wires constructor arguments.
     *
     * @param string $objectName
     * @return object
     * @throws \ReflectionException
     */
    private function make(string $objectName) : object {
        $reflection = new \ReflectionClass($objectName);

        if (!$reflection-&gt;isInstantiable()) {
            exit($reflection-&gt;getName() . ' can\'t be instantiated.');
        }

        $arguments = $this-&gt;resolveArguments($reflection);

        if (count($arguments) &lt; 1) {
            return $reflection-&gt;newInstance();
        }
        else {
            return $reflection-&gt;newInstanceArgs($arguments);
        }
    }

    /**
     * Creates an array of arguments from a reflection class.
     * Uses default value if there is one, auto-wires the object if not.
     *
     * @param $reflection
     * @return array
     */
    private function resolveArguments($reflection) : array {
        $constructor = $reflection-&gt;getConstructor();
        $parameters = $constructor-&gt;getParameters();

        if (!$parameters) {
            return $reflection-&gt;newInstance();
        }

        $arguments = [];

        foreach ($parameters as $parameter) {
            if ($parameter-&gt;isDefaultValueAvailable()) {
                $arguments[] = $parameter-&gt;getDefaultValue();
                continue;
            }

            if ($parameter-&gt;getClass() == null) {
                exit($parameter-&gt;name . ' on ' . $reflection-&gt;getName() . ' needs a default value');
            }

            $arguments[] = $this-&gt;fetch($parameter-&gt;getClass()-&gt;getName());
        }

        return $arguments;
    }
}
Naming

class Rosa tells me nothing. The name should scream out that it is a dependency injection container.

PSR

There is a standard interface for DI containers.
https://github.com/php-fig/container/blob/master/src/ContainerInterface.php

You might want to have you container implement it.

Exit vs. Exception

Implementing the PSR interface would automatically answer your question about exit vs. exception. In short, yes throw exception. Don't call exit generaly from anywhere.

Shared vs one-time services

Your container keeps references of services that you have registered but will instantiate a new one every time if not registered explicitly.

This is rather unintuitive. Consumers of the container should either be able to choose whether a service is shared or should get new instance every time. Or they should always receive consitent behaviour for all services (only one of them is supported).

On demand instantiation

Assuming you have chosen to support shared services, it is common that only a recipe for the service is provided and the actual service object is instantiated on first demand.

Configuration of Services

It would be more precise to say that your container does not just have autowiring feature, but that the autowiring is almost unevitable. You either register instantiated service (for which you have had to resolve dependencies manually) or it autowires all constructor arguments (potentialy recursively).

This is somewhat related to the previous section. If you had recipes for service instantiation, you could define which values parameters should be autowired and which should receive value defined by the consumer.

Services Dependant on Interfaces

You assume that a constructor parameter is a class or a primitive type with default value. Recipes could again help with primitives without default.

If it is not a primitive it still does not mean it is a class, it can be an interface. But you dont have any mechanism autowire interfaces because you dont keep track of interfaces implemented by services.

Service Identification

You save services by class name, making it impossible to have multiple services of the same class. It is common that it is possible to set a name for a service. Again this could be property of a service recipe.

Single Responsibility Principle

Autowiring is quite a big topic and it could be useful in conjunction with other things that a DI container. Therefore it makes sense to separate the autowiring mechanism to a separate class and have the container just depend on it.