Зачем ограничивать наследование с помощью final? Часть 3

Зачем ограничивать наследование с помощью final? Часть 3

https://habr.com/ru/post/482154/


Часть 1 тут

https://telegra.ph/Zachem-ogranichivat-nasledovanie-s-pomoshchyu-final-01-08


Часть 2 тут

https://telegra.ph/Zachem-ogranichivat-nasledovanie-s-pomoshchyu-final-CHast-2-01-08


Класс должен быть подготовлен к наследованию


Каждый класс имеет набор типичных вариантов использования: создание и уничтожение объектов, вызов методов, доступ к объектам класса через доступные интерфейсы, сериализация и десериализация, преобразование в строку, клонирование и т.д. Однако о доступности класса для наследования разработчик думает в последнюю очередь.


По умолчанию в PHP все классы доступны для наследования, а значит, если вы не пометили класс ключевым словом final, то обязаны предусмотреть все варианты его использования в дочерних классах. А таких вариантов очень много, ведь, как говорилось выше, наследование нарушает принцип сокрытия. Больше всего проблем в будущем может возникнуть с дочерними классами, которые переопределяют поведение методов родительского класса. Особенно если это не предусматривалось создателем родительского класса. Такие дочерние классы могут вызвать как проблемы внутри – целостность класса и его инварианты могут быть нарушены, так и проблемы извне – они будут несовместимы с ожидаемым поведением объектов родительского класса.


Для того чтобы избежать перечисленных проблем, нефинальные классы должны раскрыть для своих потомков все существенные нюансы внутренней реализации. Это подразумевает документирование в PHPDoc таких деталей кода, которые позволят без последствий переопределить в процессе наследования нефинальные методы класса. Для этого документация должна раскрывать: как работают нефинальные методы и как они используются через открытую рекурсию.


То есть с одной стороны, вам следует указать ожидаемую реализацию поведения для каждого метода, доступного для переопределения (т.е. нефинального, с модификатором public или protected). В PSR-19: PHPDoc tags, находящемся в состоянии черновика, не предусмотрен тег для описания требований к реализации. И часто эти требования явно не выделяются в PHPDoc, а просто предваряются фразой «Реализация этого метода делает то-то».


Однако предлагаю позаимствовать из JavaDoc тег @implSpec, который как раз предназначен для описания спецификации реализации и отделения ее от остальной документации. Как правило, PHPDoc является спецификацией API и описывает контракт между методом класса и его клиентом, т.е. внешний public интерфейс. Тег @implSpec предназначен для раскрытия подробностей того, как реализован этот API. Именно здесь предлагаю разместить текстовое описание деталей реализации, которые являются частью protected интерфейса между методом и дочерними классами.


class CommentBlock
{
    /* ... */

    /**
     * Получить строковое представление комментария для вывода в шаблон
     * Переопределение данного метода позволяет добавить дополнительную
     * функциональность (например, логирование или подсчет просмотров)
     *
     * @implSpec Реализация извлекает комментарий с ключом `$key`
     * из внутреннего массива `$this->comments` и вызывает для него метод `view()`
     *
     * @param string $key Ключ комментария
     * @return string Строковое представление комментария
     */
    public function viewComment(string $key): string
    {
        return $this->comments[$key]->view();
    }
}


В PHPDoc с помощью тега @implSpec включено описание семантики нефинального метода. Разработчик дочернего класса проинформирован о смысле параметра $key и особенностях его использования внутри метода. Деталью реализации является также наличие побочного эффекта – вызов для комментария метода view().


И если в дочернем классе потребуется переопределить метод, то спецификация реализации в @implSpec подскажет:


  • каким минимальным требованиям должна удовлетворять реализация перекрывающего метода (в части использования параметров, побочных эффектов, последовательности вызова нефинальных методов, поддерживаемых инвариантов и т.д.).
  • какое воздействие будет оказано на дочерний класс при вызове метода родительского класса через parent::method().


Теперь разберем, как задокументировать «самоиспользование» нефинальных методов. Документация методов, выполняющих self-call вызов (через $this) нефинальных методов, доступных для перекрытия в дочерних классах, должна объяснять:


  • какие нефинальные методы вызываются в теле через $this;
  • порядок вызова нефинальных методов;
  • подробности использования возвращенного значения.


class CommentBlock
{
    /* ... */

    /**
     * Получить представление всех комментариев в блоке в виде одной строки
     * 
     * @implSpec Выполняет итерирование массива `$this->comments` 
     * и для каждого элемента выполняет вызов метода `$this->viewComment()`. 
     * Возвращенные строковые значения конкатенируются в одну строку.
     * 
     * @return string Строковое представление всех комментариев
     */
    final public function viewComments(): string
    {
        $view = '';
        foreach ($this->comments as $key => $comment) {
            $view .= $this->viewComment($key);

        }
        return $view;
    }
}


Обратите внимание, что метод viewComments() описан как final и его поведение не может быть переопределено. Но он использует собственный нефинальный метод viewComment(), а потому документация должна разъяснять порядок этого использования. И разработчик дочернего класса из документации может понять:


  • как влияет переопределение viewComment() на поведение viewComments();
  • как может подключаться дополнительное поведение к viewComments() через переопределение viewComment().


Это частично решает проблемы наследования, выявленные в начале статьи. Сокрытые детали поведения методов теперь явно задокументированы, в том числе использование открытой рекурсии, и должны приниматься во внимание при проектировании классов. Проблема «хрупкости базового класса», показанная ранее на примере CommentBlock и CountingCommentBlock, становится контролируемой. Разработчик дочернего класса явно осведомлен о вызовах метода viewComment() в реализации метода viewComments() и, владея этой информацией, может предотвратить задвоение просмотров в счетчике.


Но не увлеклись ли мы слишком, документируя все подряд, вплоть до нюансов реализации. Ведь PHPDoc должен описывать только публичное API и контракты между методами класса и его клиентами: что делает метод и что не делает, какие принимает параметры и возвращает результаты, как обрабатывает исключительные ситуации.


К сожалению, наследование нарушает принцип сокрытия и, чтобы хрупкая архитектура наследования, зацепленная на детали реализации, не была случайно поломана, мы вынуждены раскрыть в документации внутреннюю логику работы нефинальных методов и методов, их «самоиспользующих». И это еще одна проблема наследования – проблема его документирования. Ведь PHPDoc зачастую уже и так перегружены подробностями, и дополнительная информация по реализации еще существенней засоряет ее.


Итак, открытость к наследованию не только предъявляет дополнительные требования к документированию класса, но и заставляет программиста следовать спецификации реализации в течении всего его существования. Гораздо проще – просто ограничить создание дочерних классов с помощью ключевого слова final, особенно если класс целенаправленно не разрабатывался для наследования. А ведь таковыми являются большинство обычных неабстрактных классов. Тем самым вы даете в его отношении четкий сигнал – «этот класс не разрабатывался для наследования и не документирован для такого варианта использования».


Поэтому при проектировании класса вам следует выбрать один из двух вариантов:


  • класс спроектирован для наследования. И в этом случае он должен документировать все возможные варианты использования в дочерних классах;
  • класс отмечен с помощью ключевого слова final. Наследование невозможно.


Многие разработчики, однако, считают написание final чересчур громоздким и ухудшающим читаемость кода. Открытые для наследования классы без final как будто уже заранее подразумевают, что будут впоследствии использованы в качестве родительского класса. Не подготовив такие классы к наследованию и не ограничив его, вы делаете их потенциально уязвимыми в будущем. И стоит ли держать в голове мысленное ограничение наследования, когда вы его можете явно выразить в коде?


С помощью final вы очень легко отсекаете всю массу проблем с наследованием и ее поддержкой. Это чрезвычайно актуально для классов, которые распространяются как часть публичной библиотеки, использование которой вы не контролируете. Как ни странно, если впоследствии вам потребуется создать дочерние классы, используя final вы все равно получаете преимущества, о которых ниже.


final заставляет задуматься о необходимости наследования


Запрещая наследование для своих классов «по умолчанию», вы получаете массу преимуществ, особенно на этапе поддержки приложения, который, как известно, гораздо длиннее самой разработки. Упрощается рефакторинг, снимается бремя поддержки обратной совместимости protected интерфейса для дочерних классов. В конце концов, ключевое слово final – это важное знание о том, что дочерние классы отсутствуют, а значит код и поведение, которые не входят в его public контракт, могут изменяться без последствий.


Но вот вы действительно столкнулись с сущностью, которая является подтипом другой, иерархически и неразрывно связана с ней. Вы оценили, что эти сущности будут изменяться вместе и сильное зацепление через отношение наследования, как нельзя лучше, выражает эту связь. Никаких проблем – вы просто удаляете final.


Можно ли также легко закрыть класс для наследования, если по умолчанию все классы в вашем приложении не используют final? Нет. Возможно кто-то из ваших коллег, а может быть и членов Opensource комьюнити, если ваш проект публичный, уже создал дочерний класс. Но чтобы точно это выяснить потребуется уже анализ кода или использование автоматических средств IDE. И то, это возможно только в случае закрытого кода в рамках внутренних проектов.


Однако, более важно, что удаление ключевого слова final сигнализирует о запуске процесса наследования класса. Если ваши классы не используют final по умолчанию, то вы никаким образом не можете выявить момент начала использования класса в качестве родительского. И это момент может быть важен для дальнейшего развития архитектуры.


final заставляет задуматься разработчика о том, стоит ли вообще идти по пути такого сильно зацепления двух классов. Может быть стоит предпочесть агрегацию и слабое зацепление через public интерфейс? А может быть архитектура приложения выстроена неверно и необходимость наследования – один из первых запахов кода, построенного на антипаттернах?


Делая осознанный выбор в пользу наследования, разработчик также принимает на себя все обязательство по подготовке родительского класса. Как вы помните, запуск наследование не ограничивается удалением final, требуется еще дополнить документацию деталями реализации, существенными для protected контракта между классами.


final теперь является важным инструментом вашей кодовой базы, а значит способ и применение этого инструмента могут стать предметом обсуждения на этапе code review (ведь вы же делаете code review? ;). Например, таких.


  • Почему новый класс не использует final? Планируется создание дочерних классов? Каких?
  • Почему у класса был удален final? Действительно ли дочерний класс является подтипом родительского и должен изменяться вместе с ним? Возможно стоило выбрать слабое зацепление и агрегацию?


И все это подталкивает нас к лучшей архитектуре и более продуманным решениям. В особенности в больших проектах и больших командах, когда строгие руководства, ограничения и подробные code review способны направить развитие архитектуры в правильном направлении. Даже в руках начинающих разработчиков.


И еще небольшое замечание. Ключевое слово final появилось вместе с новой объектной моделью в PHP5. И, скорее всего, по множеству различных причин (обратной совместимости, низкого порога вхождения) классы и методы по умолчанию доступны для наследования.


А вот если бы все было наоборот, то есть классы или методы сразу были закрыты для наследования и переопределения поведения, как в некоторых других языках. Тогда мы бы получали «по умолчанию» объекты с малым количеством точек зацепления, составляющие слабозацепленную архитектуру. Например, в C# изменение поведения метода требует указания в родительском классе модификатора virtual, а в дочернем – override. Если бы и в PHP вместо final было ключевое слово extandable, то вместо вопроса «зачем мне ограничивать гибкость этого класса через final», многие разработчики в момент необходимости открыть класс для наследования выбрали бы вместо отношения extend более слабый тип связи.


Класс должен быть подготовлен к агрегации


Итак, грамотно подготовить класс к наследованию – значит задокументировать спецификации реализаций. И это еще не конец. Гораздо большая внимательность требуется также и в момент самого наследования. Требуется принимать во внимание спецификации реализаций и учитывать взаимозависимости деталей поведения родительского и дочернего классов.


Агрегация также требует подготовки. Однако, здесь все наоборот. Документирование реализации не требуется, детали реализации в этом случае не просачиваются наружу. Для того чтобы функциональность final класса могла быть расширена, в коде требуется описать контракт класса, которому будут следовать классы-декораторы. Другими словами, обязательно должен быть предусмотрен интерфейс, который будут через implements реализовывать все связанные через агрегацию классы.


В целом, общая схема создания слабозацепленной архитектуры приложения без наследования, на базе final классов и агрегации состоит из следующих шагов.


  1. Любой класс вводится в архитектуру с ключевым словом final и с ограничением наследования:

final class SimpleCommentBlock
{
   /* ... */ 

   public function getCommentKeys(): array
   {
       /* ... */
   }

   public function viewComment(string $key): string
   {
       /* ... */
   }

   public function viewComments(): string
   {
       /* ... */
   }
}

  1. Далее вы сталкиваетесь с задачей расширить функциональность этого класса. Как сказано выше, final ограничивает вас в наследовании и заставляет пойти по пути агрегации. Вы анализируете исходный класс и выделяете поведение в базовом классе, на которое будет опираться производный класс. Далее формируете контракт этого поведения и описываете его формально в виде интерфейса.

  2. В крайнем случае, вы можете собрать сигнатуры всех public методов и составить интерфейс из них. Однако, чаще всего имеет смысл ограничить размер интерфейса и зацепить производные классы только на то поведение, которое они действительно будут использовать.

  3. В случае наследования вы пропустили бы этот шаг, не выразили в коде контракт и затащили в дочерний класс всю реализацию из родительского.

interface CommentBlock
{
   public function getCommentKeys(): array;

   public function viewComment(string $key): string;

   public function viewComments(): string;
}

  1. Реализуете интерфейс в исходном классе.

final class SimpleCommentBlock implements CommentBlock
{
   /* ... */ 
}

  1. Добавляете в архитектуру производный класс-декоратор, расширяющий функциональность базового класса и реализующий тот же самый интерфейс. В конструктор класса-декоратора вводите через интерфейс CommentBlock экземпляр базового класса. Методы класса-декоратора выполняют вызовы методов базового класса и при необходимости дополняют его поведение.

final class CountingCommentBlock implements CommentBlock
{
   /* ... */

   private $commentBlock;

   public function __construct(CommentBlock $commentBlock /* ,... */)
   {
       $this->commentBlock = $commentBlock;
   }

   /* ... */
}


Только в случае выполнения всех шагов этой схемы вы получаете расширяемую архитектуру системы, классы которой зацеплены через интерфейс. В частности, если вы не предоставите для своего final класса интерфейс, включающий полный публичный контракт, то не сможете создать классы-декораторы и использовать агрегацию. А это значит, что придется удалить из описания класса ключевое слово final и использовать наследование.


Особенно важно наличие интерфейсов для final классов, которые поставляются как часть публичной библиотеки. Если интерфейс отсутствует, то пользователь библиотеки не сможет расширить функциональность через агрегацию в форме паттерна «Декоратор». То есть так, чтобы базовый и производный класс могли полиморфно замещаться в коде. При этом добавление интерфейса или удаление ключевого слова final во внешней подключаемой библиотеке будут невозможны.


Использование final классов в тестах


В этой отличной идее создания слабозацепленной архитектуры на final классах и агрегации есть небольшая загвоздка – большинство библиотек модульного тестирования (PHPUnit, Mockery) используют наследование для создания тестовых «двойников» (test doubles). И это может стать проблемой для тех, кто в модульных тестах мокает зависимости для имитации контекста тестируемого класса.


Например, следующий тест:


final class SimpleCommentBlockTest extends TestCase
{
    public function testCreatingTestDouble(): void
    {
        $mock = $this->createMock(SimpleCommentBlock::class);
    }
}


завершается с ошибкой:


Class "SimpleCommentBlock" is declared "final" and cannot be mocked.


И это естественно, так как «под капотом» PHPUnit пытается создать дочерний класс, такого вида:


class Mock_SimpleCommentBlock_591bc3f3 extends SimpleCommentBlock
{
    /* ... */
}


Наследование очень удобно для создания тестовых «двойников». Во-первых, полученная сымитированная реализация может быть подставлена в код вместо оригинальной реализации, т.к. является ее подтипом. А, во-вторых, наследование нарушает принцип сокрытия, а значит PHPUnit может с легкостью подменить методы тестируемого класса. Например, заменить их заглушками или дополнить ожиданиями.


Ключевое слово final пресекает попытки библиотек модульного тестирования залезть в реализацию оригинального класса и подправить поведение его методов. И тут есть два подхода к преодолению этого ограничения: архитектурный и магический. Рассмотрим их поподробней.


Архитектурный подход


Вначале нужно подумать – стоит ли вообще создавать тестовый «двойник» для этого класса. Например, сущности предметной области (вроде PostComment) или объекты-значения (value object) являются стабильными (stable) внутренними зависимостями с одной конкретной реализацией. А потому должны в тестах использоваться напрямую. Это соответствует стилю classical TDD (а не mockist TDD)


Но допустим, что ваш класс является изменчивой (volatile) зависимостью и может иметь несколько возможных реализаций. В этом случае, нужно понять, что тестовый «двойник» – это всего лишь еще одна, упрощенная фиктивная реализация. А это значит, что тестовый «двойник» не должен наследовать и переопределять поведение оригинального класса, он должен разделять с ним общий интерфейс. И если архитектура системы построена на базе принципа инверсии зависимостей (DIP), а элементы зависят только от абстракций, то тестовый «двойник» сможет полиморфно замещать оригинальный класс без наследования.


А значит, если вы использовали описанную в предыдущем разделе схему подготовки класса к агрегации. Т.е. описали контракт в виде интерфейса:


interface CommentBlock
{
    /* ... */
}


и реализовали его в классе:


final class SimpleCommentBlock implements CommentBlock
{
    /* ... */
}


То сможете без проблем создать тестовый «двойник»:


final class CommentBlockTest extends TestCase
{
    public function testCreatingTestDouble(): void
    {
        $mock = $this->createMock(CommentBlock::class);
    }
}


И если у вас возникла потребность создать тестовый «двойник» на базе конкретной реализации, то это сигнал о проблеме в архитектуре. Наиболее частые из них:


  • отсутствует необходимый интерфейс для создания тестового «двойника»;
  • интерфейс не включает необходимые методы и его необходимо расширить;
  • тестовый «двойник» не может быть подставлен вместо конкретной реализации, т.к. нарушен DIP и классы зависят не от абстракций.


Магический подход


Но встречаются кейсы, когда создание тестового «двойника» – это отдельный тестовый вариант использования класса, не связанный с решением бизнес-задач. Например, вам нужно проверить ожидания для сущности предметной области (количество вызовов метода, переданные параметры). И если вы специально для этой задачи введете интерфейс в архитектуру, то он вместо «контракта для задач клиентов» становится «набором методов для проверки ожиданий».


Получается, что на этапе тестирования приложения ограничение наследования с помощью final должно быть снято.


Один вариант – использовать агрегацию и тестовый прокси-двойник. В этом случае экземпляр оригинального класса помещается внутрь тестового прокси-двойника. Вызовы, поступающие к прокси-объекту, дополняются ожиданиями и перенаправляются к оригинальному объекту. Эта идею можно реализовать вручную или воспользоваться уже готовой реализацией в библиотеке Mockery.


class SimpleCommentBlockTest extends TestCase
{
    public function testCreatingProxyDouble()
    {
        /* Экземпляр оригинального класса */
        $simpleCommentBlock = new SimpleCommentBlock();

        /* Тестовый прокси-двойник */
        $proxy = Mockery::mock($simpleCommentBlock);

        /* Подмена поведения метода */
        $proxy->shouldReceive('viewComment')
              ->andReturn('text');

        /* Проверка подмены поведения метода */
        $this->assertEquals('text', $proxy->viewComment('1'));

        /* `$proxy` прокси  не является экземпляром `SimpleCommentBlock` класса */
        $this->assertNotInstanceOf(SimpleCommentBlock::class, $proxy);
    }
}


Ограничение такой реализации очевидно – прокси-двойник не является подтипом оригинального класса и поэтому не может его замещать в коде. Как подтверждает тест, он не проходит проверку оператором instanceof. И если вы активно пользуетесь объявлениями типов (type declarations), то прокси-класс использовать в коде не получится.


Остается применить «швейцарский нож» – магию PHP, с помощью которой вы можете вскрыть любые ограничения, декларированные в коде. И уже имеется готовая маленькая библиотека Bypass Finals, которая ставит хак на загрузку файла с классом и на лету удаляет final из исходного кода. Достаточно подключить библиотеку через composer и включить удаление final до загрузки файла с тестируемым классом:


public function testUsingBypassFinals(): void
{
    /* Включить удаление `final` */
    BypassFinals::enable();

    $mock = $this->createMock(SimpleCommentBlock::class);
}


Инструменты для удобной работы с final классами


Итак, в PHP все классы по умолчанию открыты для наследования. Чтобы их закрыть от наследования «по умолчанию» и подтолкнуть к слабому зацеплению, требуется каждый раз набирать этот final в заголовке. А что, если заставить IDE автоматически добавлять ключевое слово final в заголовок каждого нового класса.


PHPStorm для генерации кода


В PHPStorm для этого необходимо настроить шаблон нового класса. Для этого в окне настроек File | Settings | Editor | File and Code Templates на закладке Files правим встроенный шаблон PHP Class. Дополняем заголовок класса в шаблоне ключевым словом final.


Правка встроенного шаблона `PHP Class`



Теперь при создании класса через File | New | PHP Class автоматически получаем заготовку класса вида:


final class SimpleCommentBlock
{

}


Допустим, вы наполнили класс методами. Далее вы сталкиваетесь с задачей расширения функционала класса через агрегацию. И следующий шаг для этого – извлечение интерфейса.


И здесь у PHPStorm также есть удобный инструмент Refactor | Extract | Interface. В окне указываем имя извлекаемого интерфейса и методы, включаемые в него. Включаем замену ссылок на класс по коду ссылками на интерфейс (опция Replace class reference with interface where possible) и перемещение PHPDoc в интерфейс (опция Move PHPDoc).


В результате рефакторинга получаем интерфейс вида:


interface CommentBlock
{
    /** PHPDoc */ 
    public function viewComment(string $key): string;
}


К исходному классу автоматически добавляется сгенерированный интерфейс:


final class SimpleCommentBlock implements CommentBlock
{
    public function viewComment(string $key): string
    {
        /* ... */
    }
}


Далее через инструмент создания File | New | PHP Class по шаблону создаем финальный класс-декоратор, расширяющий функциональность. Вручную вписываем приватное свойство для хранения экземпляра декорируемого класса и реализацию сгенерированного интерфейса:


final class CountingCommentBlock implements CommentBlock
{
    /** @var CommentBlock */
    private $commentBlock;
}


Теперь воспользуемся инструментом для генерации конструктора Code | Generate | Constructor. В результате получаем готовый конструктор для инъекции декорируемого класса.


final class CountingCommentBlock implements CommentBlock
{
    /* ... */

    public function __construct(CommentBlock $commentBlock)
    {
        $this->commentBlock = $commentBlock;
    }
}


И последний шаг – генерация заготовок для реализации методов интерфейса. Воспользуемся инструментом Code | Generate | Implement Methods. К сожалению, сейчас мы можем сгенерировать только пустые заглушки методов. Возможно в будущем в PHPStorm появится инструмент для генерации готовых «однострочных» методов передачи для делегирования поведения вложенному объекту, как это уже реализовано в родственных IntelliJ IDEA и ReSharper.


final class CountingCommentBlock implements CommentBlock
{
    /* ... */

    /**
     * @inheritDoc
     */
    public function viewComment(string $key): string
    {
        // TODO: Implement viewComment() method.
    }
}


PHPDoc для методов также сгенерированы автоматически. Осталось только наполнить методы поведением.


PHPStan для контроля стиля кодирования


А что если вы хотели бы автоматически контролировать, что добавляемые в архитектуру классы изначально ограничены в использовании наследования. Например, в самом простейшем случае, проверять, что классы объявлены с модификатором final и нацелены на агрегацию. И тут на помощь приходят инструменты статического анализа кода (подробный обзор на хабр).


Самый популярный из таких инструментов PHPStan. И «из коробки» он умеет выявлять в вашем коде типичные ошибки. Однако PHPStan позволяет довольно легко расширять функциональность и писать собственные правила проверки кодовой базы. Эту фичу как раз можно задействовать для контроля стиля своего кода.


В качестве базовой заготовки можно взять правило FinalRule из сторонней библиотеки localheinz/phpstan-rules. Класс правил реализует интерфейс PHPStan\Rules\Rule и контролирует наличие ключевого слова в методе processNode().


Довольно просто реализуются и параметры правил. Например, в правиле FinalRule можно включить возможность использования абстрактных классов и паттерна «Шаблонный метод» через параметр allowAbstractClasses. А имена классов, для которых наследование разрешено, можно указать через параметр classesNotRequiredToBeAbstractOrFinal.


Чтобы воспользоваться этими библиотеками для контроля стиля кодирования в проекте, устанавливаем их через composer:


composer require --dev phpstan/phpstan
composer require --dev localheinz/phpstan-rules


Подключаем правило FinalRule в конфигурационном файле phpstan.neon и указываем параметры:


services:
   -
      class: Localheinz\PHPStan\Rules\Classes\FinalRule
      arguments:
         allowAbstractClasses: true
         classesNotRequiredToBeAbstractOrFinal: []
      tags:
         - phpstan.rules.rule


И запускаем анализ кодовой базы с указанием уровня строгости (здесь max):


vendor/bin/phpstan -lmax analyse src


В результате получаем ошибки вида:


 ------ ------------------------------------------------------------------------
  Line   CommentBlock.php
 ------ ------------------------------------------------------------------------
  10     Class CommentBlock is neither abstract nor final.
 ------ ------------------------------------------------------------------------


Удобнее настроить вывод в JSON файл и использовать результат в Continuous Integration.


Часть 4 Заключение тут

https://telegra.ph/Zachem-ogranichivat-nasledovanie-s-pomoshchyu-final-CHast-4-Zaklyuchenie-01-08

Report Page