protected function instantiate($spec, $id = null, $type = null)
    {
        if (is_string($spec)) {
            $spec = array('class' => $spec);
        }
        $class = $spec['class'];

        // create the object, using any constructor bindings
        $constructorParams = array();
        if (isset($spec['constructor']) && is_array($spec['constructor'])) {
            $constructorParams = $spec['constructor'];
        }

        // If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton
        // flag as second argument
        if ((!$type || $type !== self::PROTOTYPE)
            && empty($constructorParams)
            && is_subclass_of($class, DataObject::class)) {
            $constructorParams = array(null, true);
        }

        $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator();
        $object = $factory->create($class, $constructorParams);

        // Handle empty factory responses
        if (!$object) {
            return null;
        }

        // figure out if we have a specific id set or not. In some cases, we might be instantiating objects
        // that we don't manage directly; we don't want to store these in the service cache below
        if (!$id) {
            $id = isset($spec['id']) ? $spec['id'] : null;
        }

        // now set the service in place if needbe. This is NOT done for prototype beans, as they're
        // created anew each time
        if (!$type) {
            $type = isset($spec['type']) ? $spec['type'] : null;
        }

        if ($id && (!$type || $type !== self::PROTOTYPE)) {
            // this ABSOLUTELY must be set before the object is injected.
            // This prevents circular reference errors down the line
            $this->serviceCache[$id] = $object;
        }

        // now inject safely
        $this->inject($object, $id);

        return $object;
    }