ReflectionException, брошенное на обновление композитора для файла, который существует

Чтобы сделать это более интересным, все работает отлично, если я запускаю composer dump-autoload -o, но мне любопытно, почему это вызовет ошибку при запуске composer update в первую очередь? Мне нужно разобраться в этом. Быстрое исправление не делает меня счастливым внутренне.

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(помещается в 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\[email protected]'
        );

        $events->listen(
            'Saveeo\Board\Observers\Events\BoardHasBeenUpdated',
            'Saveeo\Board\Observers\[email protected]'
        );

    }
}

EventServiceProvider.php(помещается в 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);

        //
    }
}

Вот структура папок. Вы не видите здесь ничего плохого?

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"
    }
}

Ответ 1

Похоже, мы изменили пространство автозагрузки (вручную или с помощью artisan app:name) для классов в каталоге app/из App (по умолчанию) в Saveeo в composer.json:

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

Это прекрасно, когда мы понимаем последствия. Мы можем увидеть проблему, описанную в вопросе, когда мы рассмотрим структуру папок проекта для файла, который вызывает исключение (с пространством имён в круглых скобках):

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

Для совместимости с PSR-4 пространство имен для BoardEventListener должно быть Saveeo\Saveeo\Board\Observers, потому что оно существует в каталоге Saveeo/, вложенном в app/. Автозагрузка PSR-4 реализует файлы классов на основе имен файлов и путей, не пространства имен, объявленные в файлах. [Composer действительно читает пространство имен из файлов классов для создания оптимизированной карты классов, если совпадает с пространством имен верхнего уровня. См. Обновление.]

Если мы не хотим изменять структуру каталогов приложения, и мы также не хотим использовать два Saveeo в пространстве имен, мы можем настроить Composer для объединения обоих каталогов в одно и то же пространство имен при его автозагрузке классы:

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

... и запомните composer dump-autoload.

Это работает, но я не рекомендую это на практике. Он отклоняется от стандарта PSR-4, делает приложение восприимчивым к конфликтам пространства имен и может смущать других людей, работающих над проектом. Я предлагаю вам сгладить пространство имен и каталог Saveeo или выбрать другое пространство имен для классов во вложенном каталоге.

... все работает отлично, если я запускаю composer dump-autoload -o, но мне любопытно, почему это вызовет ошибку при запуске composer update...?

При создании файла загрузки автозагрузки Composer фактически не выполняет ваш код. Однако команда artisan optimize, которая выполняется после composer update в большинстве приложений Laravel (до 5.5), загружает приложение для выполнения своих операций, поэтому код проблемы выполняется, и мы видим исключение.

Update:

Если использование Saveeo\Board\etc было неправильным по сравнению с выполнением Saveeo\Saveeo\Board\etc, тогда другие файлы вокруг всего приложения не будут работать, но они это делают.

Мы можем технически использовать пространства имен без PSR-4, которые не соответствуют структуре каталога проекта, когда мы создаем оптимизированный автозагрузчик, как мы хотели бы подготовить приложение для производства:

composer dump-autoload --optimize 

Это работает, потому что Composer будет сканировать каждый файл класса для пространств имен, чтобы создать статическую карту классов. Если мы не укажем --optimize, Composer полагается на динамическую автозагрузку, которая соответствует путям файлов в пространствах имен, поэтому нестандартные пространства имен или структуры каталогов не могут быть разрешены.

Пространство имен для рассматриваемого проекта работает по большей части, хотя оно не соответствует PSR-4, потому что мы вручную сбросили оптимизированный автозагрузчик, используя опцию -o для dump-autoload. Тем не менее, мы видим сообщение об ошибке, потому что composer update удаляет кэшированную карту классов перед запуском обновления, поэтому к моменту запуска команды artisan optimize в classmap больше не содержатся классы, которые мы сбросили.

Мы можем настроить Composer всегда оптимизировать автозагрузчик, установив директиву конфигурации optimize-autoloader в composer.json:

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

..., который должен исправить команды install и update для этого проекта. Это немного взломать, чтобы получить нестандартное пространство имен для решения. При таком подходе помните, что нам нужно будет сбрасывать автозагрузчик всякий раз, когда мы добавляем новый файл в разработку, который не следует за PSR-4.

Ответ 2

Это, похоже, связано с автозагрузкой композитора PSR. По умолчанию Laravel будет включать

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

что означает, что "корень пространства имен App находится в папке App и оттуда сопоставляет пространство имен в структуре папок".

Ваше пространство имен Saveeo не зарегистрировано (оно не находится в иерархии App), поэтому Composer не знает его местоположения.

Он должен работать, если вы добавите еще одну строку в ваш composer.json(а затем dump-autoload)

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

В качестве альтернативы вы могли бы, как указывает другой ответ, просто разместить целое пространство имен Saveeo в App (например, это App\Saveeo\Board\Observers\Events\BoardHasBeenCreated). Однако ваше пространство имен Saveeo кажется своего рода автономным модулем, может быть разумнее иметь его как отдельное пространство имен, а не переименовывать пространство имен во всех его файлах.

Ответ 3

Я верю, что если вы просто измените свое пространство имен на:

App\Saveeo\Board\Observers вместо Saveeo\Board\Observers

все ваши проблемы должны быть решены. Пожалуйста, дайте мне знать, если у вас есть причина для создания этого нового пространства имен вместо того, чтобы просто разветвляться из базового пространства имен App.