2

I am having a problem with my namespace fallbacks and using PSR-4 loader in Composer.

What I am trying to do is this:

  1. Have a core which can overwritten / extended.
  2. The core is based off an interface.

The directory structure is like so:

site/app/View/Example.php
site/src/ACME/app/View/Example.php
site/src/ACME/app/Interface/View.php

I am not set on this configuration so if you have a better suggestion then go for it.

My composer json is like so for psr-4:

 "autoload": {
    "psr-4": {
         "ACME\\App\\Site\\" : "app/",
         "ACME\\App\\" : "src/AMCE/app/"
    }
}

I thought this would make ACME\App\Site\View fallback to ACME\App\View if the site one was not found (Note I haven't done the interface part yet...).

My code for site/app/View/Example.php is like so:

namespace ACME\App\Site\View;

class ViewExample extends View {

Which works, when I have site/app/View/View.php as well. That looks like:

namespace ACME\App\Site\View;

class View extends \ACME\App\View\View {

The site/src/app/View/View.php look like this:

namespace ACME\APP\View;

class View {

This one should use the interface (I haven't tried yet).

So what I really want to do is make it so I don't have to have site/app/View/View.php, and I don't have to have site/app/View/Example.php - it can use site/src/ACME/app/View/Example.php.

Sorry I'm new to namespaces so I may not of phrased it very well.

What I am getting at is I thought ACME\App\Site would fallback to ACME\App - it doesn't? Or I am doing it wrong? At the moment it needs all the files in place.

3 Answers3

2

Edit: Turns out I was originally wrong, it is possible to get your example working with PSR-4! You just need to specify an array of directories for namespaces that can be loaded from different places.

Easy solution

{
    "autoload": {
        "psr-4": {
            "ACME\\App\\Site\\": ["app/", "src/ACME/app"],
            "ACME\\App\\": "src/ACME/app/"
        }
    }
}

Personally, I would rather name my namespaces more explicitly, see below.

Original Answer

The composer PSR-4 loader does not fall back when trying to load files that do not exist. It just fails immediately. Its flow looks like:

  1. \ACME\App\Site\View is not loaded
  2. Scan PSR-4 entries for matching namespaces
  3. Class name matches the namespace \ACME\App\Site (your first PSR-4 entry).
  4. Load file app/View.php
  5. File does not exist. Error.

It never goes back to step 3 and tries the next namespace.

So how do I fix it?

It looks like you want to separate your reusable library code from your site code. If that's the case, I would use separate namespaces. For example, use the ACME\Site namespace to hold your reusable code, and use ACME\MySiteName for your site-specific code. Then there will be no ambiguity, and composer will have no trouble loading your classes.

But I don't want to rearrange my namespaces!

Ok, that's fine, but you'll have to use a hack to get around your problem. Composer has a classmap loader, and you'll have to use that instead of the preferred PSR-4 loader.

{
    "autoload": {
        "classmap": ["app/", "src/"]
    }
}
Justin Howard
  • 5,504
  • 1
  • 21
  • 48
  • Ok thanks for that - I thought I was doing something wrong. I read the namespace docs on the PHP site they talked about fallback so assumed composer would have fallback. I guess this is not something most people want. I am moving away from my own autoloader to this so that defeats the point so hacking seems a bit silly. – digitalanalogue Feb 19 '16 at 17:55
  • Sorry! I was wrong. I couldn't let this one rest and had to try it out. I actually found an easy solution using PSR-4. Now you can take your pick. – Justin Howard Feb 19 '16 at 23:25
  • Ok I see. Should do what I want now but I don't think it is going to be quite right so will probobly use two name spaces :) At least I know it can be done. – digitalanalogue Feb 22 '16 at 15:41
  • Are you really sure that your proposal is able to run code with a class that inherits from a class that is not there? – Sven Feb 23 '16 at 00:35
2

Let's separate the things a bit, because they are all mixed up for now.

What I am trying to do is this:

  1. Have a core which can overwritten / extended.
  2. The core is based off an interface.

This sounds like basic object oriented inheritance. An interface defines the proposed public behaviour, the core implements the needed basics, and the detail implementation changes some parts, and reuses the others.

Let's write your example code in a way PHP sees it with absolute namespace names:

class \ACME\App\Site\View\ViewExample extends \ACME\App\Site\View\View {}

class \ACME\App\Site\View\View extends \ACME\App\View\View {}

class \ACME\App\View\View {}

You have three classes explicitly named. You'd need three files that match the namespace and class name. The autoloading does not need to do any detection whether or not a class is present - because you cannot optionally inherit from a class that isn't there, or omit it otherwise.

On the other hand, implementing three levels of inheritance by default very likely is too much. It looks like bad design to me, and will make maintaining the code harder than necessary. Depending on what you want to achieve, there are plenty of alternatives to get what you want easier. For example, to change some details of behavior, there are the decorator pattern or the strategy pattern.

So what I really want to do is make it so I don't have to have site/app/View/View.php, and I don't have to have site/app/View/Example.php - it can use site/src/ACME/app/View/Example.php.

You cannot have this. Your code explicitly states that it inherits from \ACME\App\Site\View\View, so this class MUST be present somewhere.

This is independent of any autoloading. To experiment, you can add all your code into one single file and then run it. This will make all classes known to PHP immediately, and the problem will become obvious: You cannot remove a class when at the same time other classes inherit it.

Sorry I'm new to namespaces so I may not of phrased it very well.

Namespaces are nothing really fancy, the same problem would arise if you would use the PSR-0 style classnames with underscores:

class ACME_App_Site_View_ViewExample extends ACME_App_Site_View_View {}

// This class MUST be present for the above class to work
class ACME_App_Site_View_View extends ACME_App_View_View {}

class ACME_App_View_View {}

The main new feature with namespaces is that you can import one class under a second name within a file with use OtherNamespace\Classname. But this is only an alias within the scope of this file (i.e. it does not affect other files or the global scope).

Sven
  • 69,403
  • 10
  • 107
  • 109
  • Ok, that makes sense, I come from using setting include paths, so on the app site it would include the SITE/app and if the file existed and had the same class name it would load over the next include path CORE/app. This meant files that I wanted to replace locally I could. I was probobly overthinking it and forcing myself to use namespaces - and as composer had its own autoloader I thought I should try that. – digitalanalogue Feb 23 '16 at 10:38
1

Namespaces and autoloading are not the right tool for this job. A namespace is just a way of making sure two people (or parts of your code) don't use the same name to mean different things. Autoloading is just a way to avoid having to list every source file you want to load code from.

When you override the behaviour of one class in another, these are not the same class; often, you'll want to inherit the default actions and reuse parts of them.

You might want to create several sub-classes for different purposes, so you need somewhere to hold the logic of which to use. The component which deals with this is called a "service locator" or sometimes a "DI container".

Namespaces let you map short names to longer, unique class names; autoloading let's you map a specific unique class name to a source file; service location is how you choose which unique class you want to use in a specific circumstance.

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • I can only underline this answer. Using the autoloading to search for the one class in different directories is the wrong approach. – Sven Feb 23 '16 at 00:08