Правильный шаблон шаблона репозитория в PHP?

Предисловие: я пытаюсь использовать шаблон хранилища в архитектуре MVC с реляционными базами данных.

Недавно я начал изучать TDD на PHP, и я понимаю, что моя база данных слишком тесно связана с остальной частью моего приложения. Я читал о репозиториях и использовании контейнера IoC, чтобы "внедрить" его в мои контроллеры. Очень классные вещи. Но теперь есть несколько практических вопросов о дизайне хранилища. Рассмотрим следующий пример.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

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

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Проблема № 1: слишком много полей

Все эти методы поиска используют метод выбора всех полей (SELECT *). Однако в моих приложениях я всегда пытаюсь ограничить количество полей, которые я получаю, поскольку это часто увеличивает накладные расходы и замедляет работу. Для тех, кто использует этот шаблон, как вы справляетесь с этим?

Проблема № 2: слишком много методов

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

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • И т.п.

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

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

С моим подходом к хранилищу я не хочу заканчивать этим:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Проблема № 3: Невозможно сопоставить интерфейс

Я вижу преимущество в использовании интерфейсов для репозиториев, так что я могу поменять свою реализацию (для целей тестирования или для других целей). Я понимаю, что интерфейсы определяют контракт, которому должна следовать реализация. Это замечательно, пока вы не начнете добавлять дополнительные методы в свои репозитории, такие как findAllInCountry(). Теперь мне нужно обновить свой интерфейс, чтобы также иметь этот метод, в противном случае другие реализации могут его не иметь, и это может сломать мое приложение. От этого кажется безумным... случай, когда хвост виляет собакой.

Шаблон спецификации?

Это наводит меня на мысль, что в репозитории должно быть только фиксированное количество методов (таких как save(), remove(), find(), findAll() и т.д.). Но тогда как мне запустить конкретные поиски? Я слышал о IsSatisfiedBy() спецификаций, но мне кажется, что это уменьшает только весь набор записей (через IsSatisfiedBy()), который явно имеет серьезные проблемы с производительностью, если вы извлекаете данные из базы данных.

Помогите?

Ясно, что мне нужно немного переосмыслить при работе с репозиториями. Может кто-нибудь просветить о том, как это лучше всего обрабатывается?

Ответ 1

Я подумал, что я взломаю ответ на свой вопрос. Далее следует лишь один из способов решения вопросов 1-3 в моем первоначальном вопросе.

Отказ от ответственности: я могу не всегда использовать правильные условия при описании шаблонов или методов. Извините за это.

Цели:

  • Создайте полный пример базового контроллера для просмотра и редактирования Users.
  • Весь код должен быть полностью проверен и макетироваться.
  • Контроллер не должен знать, где хранятся данные (что означает его изменение).
  • Пример отображения реализации SQL (наиболее распространенный).
  • Для максимальной производительности контроллеры должны получать только необходимые данные - никаких дополнительных полей.
  • Реализация должна использовать некоторый тип преобразователя данных для упрощения разработки.
  • Реализация должна иметь возможность выполнять сложные поиски данных.

Решение

Я разделяю взаимодействие с постоянным хранилищем (базой данных) на две категории: R (чтение) и CUD (создание, обновление, удаление). Мой опыт в том, что чтение действительно приводит к замедлению приложения. И хотя манипуляции с данными (CUD) на самом деле медленнее, это происходит гораздо реже, и поэтому гораздо меньше беспокоит.

CUD (Создать, обновить, удалить) легко. Это потребует работы с фактическими моделями, которые затем передаются в мой Repositories для сохранения. Обратите внимание: мои репозитории по-прежнему будут предоставлять метод Read, но просто для создания объекта, а не для отображения. Подробнее об этом позже.

R (Читать) не так просто. Здесь нет моделей, только объекты значений. Используйте массивы если вы предпочитаете. Эти объекты могут представлять собой единую модель или смесь многих моделей, что угодно. Они не очень интересны сами по себе, но как они созданы. Я использую то, что я называю Query Objects.

Код:

Модель пользователя

Давайте начнем просто с нашей базовой модели пользователя. Обратите внимание, что нет ORM-расширения или базы данных вообще. Просто чистая модель славы. Добавьте своих геттеров, сеттеров, проверку, что угодно.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Интерфейс репозитория

Прежде чем создать свой пользовательский репозиторий, я хочу создать свой интерфейс репозитория. Это определит "контракт", который должны использовать репозитории для использования моим контроллером. Помните, что мой контроллер не знает, где хранятся данные.

Обратите внимание, что в моих репозиториях будут только три этих метода. Метод save() отвечает за создание и обновление пользователей, просто в зависимости от того, имеет ли пользовательский объект установленный идентификатор.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

Реализация хранилища SQL

Теперь для создания моей реализации интерфейса. Как уже упоминалось, мой пример будет состоять из базы данных SQL. Обратите внимание на использование data mapper, чтобы не писать повторяющиеся SQL-запросы.

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

Интерфейс объекта запроса

Теперь с CUD (создать, обновить, удалить), позаботившимся о нашем репозитории, мы можем сосредоточиться на R (Читать). Объекты запроса - это просто инкапсуляция некоторой логики поиска данных. Это не построители запросов. Абстрагируя его, как наш репозиторий, мы можем изменить его реализацию и протестировать его проще. Примером объекта запроса может быть AllUsersQuery или AllActiveUsersQuery, или даже MostCommonUserFirstNames.

Возможно, вы думаете: "Не могу ли я просто создавать методы в своих репозиториях для этих запросов?" Да, но вот почему я этого не делаю:

  • Мои репозитории предназначены для работы с объектами модели. В приложении реального мира, зачем мне понадобиться поле password, если я ищу список всех моих пользователей?
  • Хранилища часто являются специфичными для модели, но запросы часто включают более одной модели. Итак, какой репозиторий вы помещаете в свой метод?
  • Это сохраняет мои репозитории очень просто - не раздутый класс методов.
  • Все запросы теперь организованы в свои собственные классы.
  • Действительно, на данный момент репозитории существуют просто для абстрагирования моего уровня базы данных.

В моем примере я создам объект запроса для поиска "AllUsers". Вот интерфейс:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Реализация объекта запроса

Здесь мы снова можем использовать устройство отображения данных, чтобы ускорить разработку. Обратите внимание, что я разрешаю одну настройку возвращаемого набора данных - поля. Это касается, насколько я хочу, манипулировать выполненным запросом. Помните, что мои объекты запроса не являются сборщиками запросов. Они просто выполняют конкретный запрос. Однако, поскольку я знаю, что я, вероятно, буду использовать этот много, в нескольких разных ситуациях, я даю себе возможность указывать поля. Я никогда не хочу возвращать поля, которые мне не нужны!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

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

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

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

Контроллер

Теперь самое интересное - собрать все части вместе. Обратите внимание, что я использую инъекцию зависимостей. Обычно зависимости встраиваются в конструктор, но я фактически предпочитаю вводить их прямо в мои методы (маршруты) контроллера. Это минимизирует график объекта контроллера, и я на самом деле считаю его более четким. Обратите внимание, что если вам не нравится этот подход, просто используйте традиционный метод конструктора.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

Заключительные мысли:

Важно отметить, что когда я изменяю (создавая, обновляя или удаляя) объекты, я работаю с реальными объектами модели и выполняю упорство через свои репозитории.

Однако, когда я показываю (выбирая данные и отправляя их в представления), я не работаю с объектами модели, а довольно старые старые объекты значений. Я выбираю только те поля, которые мне нужны, и он создан, чтобы я мог максимально повысить производительность поиска данных.

Мои репозитории остаются очень чистыми, и вместо этого этот "беспорядок" организован в мои запросы модели.

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

Мне бы очень хотелось услышать, как вы принимаете мой подход!


Июль 2015 Обновление:

Меня спрашивали в комментариях, где я закончил все это. Ну, не так уж и далеко. Честно говоря, мне все еще не нравятся репозитории. Я нахожу их чрезмерными для базового поиска (особенно если вы уже используете ORM) и беспорядочно работаете с более сложными запросами.

Я вообще работаю с ORM ActiveRecord, поэтому чаще всего я просто ссылаюсь на эти модели непосредственно во всем приложении. Однако в ситуациях, когда у меня есть более сложные запросы, я буду использовать объекты запроса, чтобы сделать их более многоразовыми. Я также должен отметить, что я всегда добавляю свои модели в свои методы, что делает их более легкими для насмешек в моих тестах.

Ответ 2

Основываясь на моем опыте, вот несколько ответов на ваши вопросы:

Q: Как мы справляемся с возвратом полей, которые нам не нужны?

A: По моему опыту это действительно сводится к работе с полными сущностями по сравнению с специальными запросами.

Полная сущность - это что-то вроде объекта User. Он имеет свойства и методы и т.д. Он является гражданином первого класса в вашей кодовой базе.

Запрос ad-hoc возвращает некоторые данные, но мы ничего не знаем. Поскольку данные передаются вокруг приложения, это делается без контекста. Это User? A User с приложением некоторой Order информации? Мы действительно не знаем.

Я предпочитаю работать с полными объектами.

Вы правы, что часто будете возвращать данные, которые вы не будете использовать, но вы можете решить это различными способами:

  • Агрессивно кэшировать объекты, чтобы вы только платили цену за чтение один раз из базы данных.
  • Потратьте больше времени на моделирование своих объектов, чтобы у них были хорошие различия между ними. (Рассмотрим разбиение большой сущности на два небольших объекта и т.д.).
  • Рассмотрим несколько версий сущностей. У вас может быть User для задней части и, возможно, UserSmall для вызовов AJAX. Один из них может иметь 10 свойств и один имеет 3 свойства.

Недостатки работы с ad-hoc-запросами:

  • В результате вы получаете по существу одни и те же данные по многим запросам. Например, с User вы в конечном итоге напишите по существу те же самые select * для многих вызовов. Один звонок получит 8 из 10 полей, один получит 5 из 10, один получит 7 из 10. Почему бы не заменить все одним вызовом, который получает 10 из 10? Причина, по которой это плохо, заключается в том, что это убийство для повторного фактора/тестирования/макета.
  • С течением времени очень сложно рассуждать о вашем коде. Вместо утверждений типа "Почему User так медленно?" вы в конечном итоге отслеживаете одноразовые запросы, и поэтому исправления ошибок имеют тенденцию быть небольшими и локализованными.
  • Очень сложно заменить базовую технологию. Если теперь вы сохраняете все в MySQL и хотите перейти на MongoDB, гораздо сложнее заменить 100 специальных вызовов, чем несколько сущностей.

Q: У меня будет слишком много методов в моем репозитории.

A: Я не видел ничего подобного, кроме консолидации вызовов. Метод, вызываемый в вашем репозитории, действительно соответствует функциям вашего приложения. Чем больше функций, тем больше вызовов, связанных с конкретными данными. Вы можете вернуться к функциям и попытаться объединить похожие вызовы в один.

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

Иногда мне приходится говорить себе: "Ну, это должно было дать где-то! Нет серебряных пуль".

Ответ 3

Я использую следующие интерфейсы:

  • Repository - загружает, вставляет, обновляет и удаляет объекты
  • Selector - находит объекты на основе фильтров в репозитории
  • Filter - инкапсулирует логику фильтрации

My Repository - агностик базы данных; на самом деле он не указывает на постоянство; это может быть что угодно: база данных SQL, xml файл, удаленная служба, инопланетянин из космоса и т.д. Для возможностей поиска Repository создает Selector, который может быть отфильтрован, LIMIT -ed, отсортирован и подсчитан. В конце селектор выбирает из константы один или несколько Entities.

Вот пример кода:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

Затем одна реализация:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

Идея состоит в том, что общий Selector использует Filter, но реализация SqlSelector использует SqlFilter; SqlSelectorFilterAdapter адаптирует общий Filter к конкретному SqlFilter.

Клиентский код создает объекты Filter (которые являются общими фильтрами), но в конкретной реализации селектора эти фильтры преобразуются в фильтрах SQL.

Другие реализации селектора, такие как InMemorySelector, преобразуются из Filter в InMemoryFilter с использованием их конкретных InMemorySelectorFilterAdapter; поэтому каждая реализация селектора имеет собственный фильтр-адаптер.

Используя эту стратегию, мой клиентский код (на уровне bussines) не заботится о конкретной реализации репозитория или селектора.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

P.S. Это упрощение моего реального кода

Ответ 4

Я добавлю немного об этом, поскольку в настоящее время я пытаюсь понять все это сам.

# 1 и 2

Это идеальное место для вашего ORM, чтобы сделать тяжелый подъем. Если вы используете модель, которая реализует какой-то ORM, вы можете просто использовать ее методы, чтобы позаботиться об этих вещах. Создайте собственные функции OrderBy, которые реализуют методы Eloquent, если вам нужно. Использование Eloquent, например:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

То, что вы ищете, это ORM. Нет причин, по которым ваш репозиторий не может быть основан на одном. Это потребует от пользователя красноречия, но я лично не вижу в этом проблемы.

Если вы все же хотите избежать ORM, вам тогда придется "сворачивать", чтобы получить то, что вы ищете.

# 3

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

Тем не менее, я только начинаю понимать, но эти реализации помогли мне.

Ответ 5

Я могу только прокомментировать то, как мы (в моей компании) справляемся с этим. Прежде всего, производительность для нас не слишком важна, но имеет чистый/правильный код.

Прежде всего, мы определяем Модели, такие как UserModel, который использует ORM для создания объектов UserEntity. Когда a UserEntity загружается из модели, все поля загружаются. Для полей, ссылающихся на внешние объекты, мы используем соответствующую внешнюю модель для создания соответствующих объектов. Для этих объектов данные будут загружены по запросу. Теперь ваша первоначальная реакция может быть...???...!!! позвольте мне привести пример примера:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

В нашем случае $db является ORM, который может загружать объекты. Модель указывает ORM на загрузку набора объектов определенного типа. ORM содержит сопоставление и использует это для ввода всех полей для этого объекта в объект. Однако для иностранных полей загружается только идентификатор этих объектов. В этом случае OrderModel создает OrderEntity только с идентификатором ссылочных ордеров. Когда PersistentEntity::getField вызывается OrderEntity, сущность дает команду модели ленить загружать все поля в OrderEntity s. Все OrderEntity, связанные с одним UserEntity, рассматриваются как один набор результатов и будут загружены сразу.

Магия здесь заключается в том, что наша модель и ORM вводят все данные в сущности и что объекты просто предоставляют функции-обертки для общего метода getField, предоставленного PersistentEntity. Подводя итог, мы всегда загружаем все поля, но при необходимости загружаются поля, ссылающиеся на внешний объект. Просто загрузка кучи полей на самом деле не является проблемой производительности. Загрузите все возможные внешние объекты, но это будет ОГРОМНОЕ снижение производительности.

Теперь загрузите определенный набор пользователей на основе предложения where. Мы предоставляем объектно-ориентированный пакет классов, который позволяет вам указать простое выражение, которое можно склеить. В примере кода я назвал его GetOptions. Это оболочка для всех возможных вариантов выбора запроса. Он содержит коллекцию предложений, клаузула и всего остального. Наши предложения довольно сложны, но вы, очевидно, можете упростить версию.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Простейшей версией этой системы было бы передать часть WHERE запроса как строку непосредственно в модель.

Прошу прощения за этот довольно сложный ответ. Я постарался как можно скорее и четко изложить нашу структуру. Если у вас есть дополнительные вопросы, не стесняйтесь спрашивать их, и я обновлю свой ответ.

EDIT: Кроме того, если вы действительно не хотите сразу загружать некоторые поля, вы можете указать параметр ленивой загрузки в вашем ORM-сопоставлении. Поскольку все поля в конечном итоге загружаются с помощью метода getField, вы можете загрузить некоторые поля в последнюю минуту при вызове этого метода. Это не очень большая проблема в PHP, но я бы не рекомендовал для других систем.

Ответ 6

Это несколько разных решений, которые я видел. Для каждого из них есть плюсы и минусы, но вам решать.

Проблема №1: Слишком много полей

Это важный аспект, особенно когда вы берете на учет Проверки только для показа. Я вижу два решения для решения этой проблемы. Вы можете обновить свои функции, чтобы взять необязательный параметр массива, который будет содержать список возвращаемых столбцов. Если этот параметр пуст, вы должны вернуть все столбцы в запросе. Это может быть немного странно; основанный на параметре, который вы могли бы получить для объекта или массива. Вы также можете дублировать все свои функции, чтобы иметь две различные функции, которые запускают один и тот же запрос, но один возвращает массив столбцов, а другой возвращает объект.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Проблема №2: Слишком много методов

Я кратко работал с Propel ORM год назад, и это основано на том, что я помню из этого опыта. Propel имеет возможность генерировать свою структуру класса, основанную на существующей схеме базы данных. Он создает два объекта для каждой таблицы. Первый объект - это длинный список функций доступа, аналогичный тому, что вы сейчас перечисляете; findByAttribute($attribute_value). Следующий объект наследуется от этого первого объекта. Вы можете обновить этот дочерний объект для создания более сложных функций getter.

Другим решением будет использование __call() для сопоставления не определенных функций с чем-то действующим. Ваш метод __call будет способен анализировать findById и findByName в разные запросы.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Надеюсь, это поможет хотя бы кое-чему.

Ответ 8

Я согласен с @ryan1234, что вы должны передавать полные объекты в коде и использовать общие методы запросов для получения этих объектов.

Model::where(['attr1' => 'val1'])->get();

Для внешнего/конечного использования мне очень нравится метод GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}