Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReflectionException thrown on composer update for a file that exists

To make this more interesting, things work just fine if I run composer dump-autoload -o but I am curious why would this throw an error when I run composer update in the first place? I need to get to the bottom of this. A quick fix doesn't make me happy internally.

aligajani at Alis-MBP in ~/Projects/saveeo on master ✗                                                                                    [faaba41c]  4:53
> composer update
> php artisan clear-compiled
Loading composer repositories with package information
Updating dependencies (including require-dev)
Nothing to install or update
Package guzzle/guzzle is abandoned, you should avoid using it. Use guzzlehttp/guzzle instead.
Generating autoload files
> php artisan optimize


  [ReflectionException]                                           
  Class Saveeo\Board\Observers\BoardEventListener does not exist  

BoardEventListener.php (placed in Saveeo/Board/Observers)

<?php

namespace Saveeo\Board\Observers;

use Saveeo\Services\HashIds\Contracts\HashIds as HashIdService;

class BoardEventListener {
    private $hashIdService;

    public function __construct(HashIdService $hashIdService) {
        $this->hashIdService = $hashIdService;
    }

    public function whenBoardIsCreated($event) {
        $this->hashIdService->syncHashIdValueOnModelChanges($event, 'board');
    }

    public function whenBoardIsUpdated($event) {
        $this->hashIdService->syncHashIdValueOnModelChanges($event, 'board');
    }

    public function subscribe($events) {
        $events->listen(
            'Saveeo\Board\Observers\Events\BoardHasBeenCreated',
            'Saveeo\Board\Observers\BoardEventListener@whenBoardIsCreated'
        );

        $events->listen(
            'Saveeo\Board\Observers\Events\BoardHasBeenUpdated',
            'Saveeo\Board\Observers\BoardEventListener@whenBoardIsUpdated'
        );

    }
}

EventServiceProvider.php (placed in Saveeo/Providers)

<?php

namespace Saveeo\Providers;

use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * The subscriber classes to register.
     *
     * @var array
     */
    protected $subscribe = [
        'Saveeo\Board\Observers\BoardEventListener',
    ];

    /**
     * Register any other events for your application.
     *
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @return void
     */
    public function boot(DispatcherContract $events) {
        parent::boot($events);

        //
    }
}

Here is the folder structure. Can't see anything wrong here?

https://imgur.com/BI44Lq6

Composer.json

{
    "name": "laravel/laravel",
    "description": "The Laravel Framework.",
    "keywords": [
        "framework",
        "laravel"
    ],
    "license": "MIT",
    "type": "project",
    "require": {
        "php": ">=5.5.9",
        "laravel/framework": "5.2.*",
        "firebase/php-jwt": "~2.0",
        "guzzlehttp/guzzle": "5.*",
        "guzzlehttp/oauth-subscriber": "0.2.0",
        "laravel/socialite": "2.*",
        "league/flysystem-aws-s3-v3": "~1.0",
        "aws/aws-sdk-php": "3.*",
        "bugsnag/bugsnag-laravel": "1.*",
        "vinkla/hashids": "^2.3"
    },
    "require-dev": {
        "fzaninotto/faker": "~1.4",
        "mockery/mockery": "0.9.*",
        "phpunit/phpunit": "~4.0",
        "phpspec/phpspec": "~2.1",
        "tymon/jwt-auth": "0.5.*",
        "symfony/dom-crawler": "~3.0",
        "symfony/css-selector": "~3.0"
    },
    "autoload": {
        "classmap": [
            "database"
        ],
        "psr-4": {
            "Saveeo\\": "app/"
        }
    },
    "autoload-dev": {
        "classmap": [
            "tests/TestCase.php"
        ]
    },
    "scripts": {
        "post-install-cmd": [
        "php artisan clear-compiled",
        "php artisan optimize"
    ],
    "pre-update-cmd": [
        "php artisan clear-compiled"
    ],
    "post-update-cmd": [
        "php artisan optimize"
    ],
    "post-root-package-install": [
        "php -r \"copy('.env.example', '.env');\""
    ],
    "post-create-project-cmd": [
        "php artisan key:generate"
    ]
    },
    "config": {
        "preferred-install": "dist"
    }
}
like image 487
Ali Gajani Avatar asked Oct 19 '17 16:10

Ali Gajani


3 Answers

It looks like we changed the autoloading namespace (manually, or by using artisan app:name) for the classes in the app/ directory from App (the default) to Saveeo in composer.json:

"autoload": {
    "psr-4": {
        "Saveeo\\": "app/"
    }
},

This is perfectly fine to do when we understand the implications. We can see the issue described in the question when we take a look at the project's folder structure for the file that causes the exception (with the namespaces in parentheses):

app                    (Saveeo)
├── Saveeo             (Saveeo\Saveeo)
│   ├── Board          (Saveeo\Saveeo\Board)
│   │   ├── Observers  (Saveeo\Saveeo\Board\Observers)
│   │   │   ├── BoardEventListener
└── ...

For compatibility with PSR-4, the namespace for BoardEventListener should be Saveeo\Saveeo\Board\Observers because it exists in the Saveeo/ directory nested under app/. PSR-4 autoloading implementations resolve class files based on file names and paths, not the namespaces declared in the files. [Composer does read the namespace from the class files to create an optimized classmap, as long as the top-level namespace matches. See update.]

If we don't want to change the directory structure of the application, and we also don't want to use two Saveeos in the namespace, we can configure Composer to merge both directories into the same namespace when it autoloads classes:

"autoload": {
    "psr-4": {
        "Saveeo\\": [ "app/", "app/Saveeo/" ]
    }
},

...and remember to composer dump-autoload.

It works, but I don't recommend this in practice. It deviates from the PSR-4 standard, makes the application susceptible to namespace conflicts, and may be confusing to other people working on the project. I suggest that you flatten the Saveeo namespace and directory, or choose a different namespace for the classes in the nested directory.

...things work just fine if I run composer dump-autoload -o but I am curious why would this throw an error when I run composer update...?

When generating the autoloading cache file, Composer doesn't actually execute your code. However, the artisan optimize command, which runs after composer update in most Laravel applications (until 5.5), does boot the application to perform its operations, so the problem code executes and we see the exception.

Update:

If using Saveeo\Board\etc was wrong versus doing Saveeo\Saveeo\Board\etc then other files around the entire application wouldn't work, but they do.

We can technically use non-PSR-4 namespaces that don't match the project's directory structure when we generate an optimized autoloader like we would to prepare an application for production:

composer dump-autoload --optimize 

This works because Composer will scan each class file for namespaces to create a static classmap. When we don't specify --optimize, Composer relies on dynamic autoloading which matches file paths to namespaces, so non-standard namespaces or directory structures fail to resolve.

The namespacing for the project in question works for the most part, even though it doesn't follow PSR-4, because we're manually dumping an optimized autoloader by using the -o option for dump-autoload. However, we see the error message because composer update removes the cached classmap before running the update, so, by the time the artisan optimize command runs, the classmap no longer contains the classes that we dumped.

We can configure Composer to always optimize the autoloader by setting the optimize-autoloader configuration directive in composer.json:

"config": { 
    "optimize-autoloader": true 
}

...which should fix the install and update commands for this project. It's a bit of a hack to get non-standard namespacing to resolve. With this approach, remember that we'll need to dump the autoloader whenever we add a new file in development that doesn't follow PSR-4.

like image 172
Cy Rossignol Avatar answered Oct 31 '22 16:10

Cy Rossignol


This seems to be related to the Composer PSR autoloading. By default Laravel will include

"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
}

which means "the root of the App namespace is in the app folder, and from there match namespace to folder structure".

Your Saveeo namespace hasn't been registered (it's not in the App hierarchy), so Composer doesn't know its location.

It should work if you add another line to your composer.json (and then dump-autoload)

"autoload": {
    "psr-4": {
        "App\\": "app/",
        "Saveeo\\": "app/Saveeo"
    }
}

Alternatively, you could, as the other answer points out, just place the whole Saveeo namespace within App (so it's App\Saveeo\Board\Observers\Events\BoardHasBeenCreated for instance). However your Saveeo namespace seems to be sort of a self-contained module, it may be more reasonable to have it as a separate namespace rather than renaming the namespace in all of its files.

like image 25
alepeino Avatar answered Oct 31 '22 16:10

alepeino


I believe if you just change your namespace to:

App\Saveeo\Board\Observers instead of Saveeo\Board\Observers

all your problems should be solved. Please let me know if you have a reason for creating this new namespace, instead of just branching out from the base App namespace.

like image 3
Orestis Palampougioukis Avatar answered Oct 31 '22 16:10

Orestis Palampougioukis