database tag - Conception de modèle de référentiel approprié en PHP?





html name (6)


J'utilise les interfaces suivantes:

  • Repository - charge, insère, met à jour et supprime des entités
  • Selector - recherche les entités basées sur des filtres, dans un référentiel
  • Filter - encapsule la logique de filtrage

Mon Repository est agnostique de base de données; en fait, il ne spécifie aucune persistance; cela peut être n'importe quoi: base de données SQL, fichier XML, service distant, extraterrestre de l'espace, etc. Pour les capacités de recherche, le Repository construit un Selector qui peut être filtré, LIMITé, trié et compté. À la fin, le sélecteur récupère une ou plusieurs Entities de la persistance.

Voici un exemple de code:

<?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();
}

Ensuite, une implémentation:

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);
    }
}

L'idée est que le Selector générique utilise Filter mais que l'implémentation SqlSelector utilise SqlFilter ; le SqlSelectorFilterAdapter adapte un Filter générique à un SqlFilter concret.

Le code client crée des objets Filter (qui sont des filtres génériques) mais dans l'implémentation concrète du sélecteur, ces filtres sont transformés dans des filtres SQL.

D'autres implémentations de sélecteur, comme InMemorySelector , se transforment de Filter en InMemoryFilter utilisant leur InMemorySelectorFilterAdapter spécifique; Ainsi, chaque implémentation de sélecteur est livrée avec son propre adaptateur de filtre.

En utilisant cette stratégie, mon code client (dans la couche bussines) ne se soucie pas d'une implémentation de référentiel ou de sélecteur spécifique.

/** @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();

PS C'est une simplification de mon code réel

Préface: J'essaie d'utiliser le modèle de référentiel dans une architecture MVC avec des bases de données relationnelles.

J'ai récemment commencé à apprendre TDD en PHP, et je me rends compte que ma base de données est couplée beaucoup trop étroitement avec le reste de mon application. J'ai lu des dépôts et utilisé un conteneur IoC pour "l'injecter" dans mes contrôleurs. Trucs très cool. Mais maintenant, posez quelques questions pratiques sur la conception du dépôt. Considérez l'exemple suivant.

<?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)
    {
    }
}

Issue # 1: Trop de champs

Toutes ces méthodes de recherche utilisent une approche de sélection de tous les champs ( SELECT * ). Cependant, dans mes applications, j'essaie toujours de limiter le nombre de champs que je reçois, car cela ajoute souvent des frais généraux et ralentit les choses. Pour ceux qui utilisent ce modèle, comment gérez-vous cela?

Question n ° 2: Trop de méthodes

Bien que cette classe soit agréable en ce moment, je sais que dans une application du monde réel, j'ai besoin de beaucoup plus de méthodes. Par exemple:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Comme vous pouvez le voir, il pourrait y avoir une très, très longue liste de méthodes possibles. Et puis, si vous ajoutez le problème de sélection des champs ci-dessus, le problème s'aggrave. Dans le passé, je mettais normalement toute cette logique dans mon contrôleur:

<?php

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

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

}

Avec mon approche de référentiel, je ne veux pas finir avec ceci:

<?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))
    }

}

Problème n ° 3: impossible de faire correspondre une interface

Je vois l'avantage d'utiliser des interfaces pour les référentiels, donc je peux échanger ma mise en œuvre (à des fins de test ou autre). Ma compréhension des interfaces est qu'elles définissent un contrat qu'une implémentation doit suivre. C'est génial jusqu'à ce que vous commenciez à ajouter des méthodes supplémentaires à vos dépôts comme findAllInCountry() . Maintenant, j'ai besoin de mettre à jour mon interface pour avoir aussi cette méthode, sinon d'autres implémentations peuvent ne pas l'avoir, et cela pourrait casser mon application. Par cela se sent fou ... un cas de la queue qui remue le chien.

Modèle de spécification?

Cela m'amène à croire que repository ne devrait avoir qu'un nombre fixe de méthodes (comme save() , remove() , find() , findAll() , etc). Mais alors comment puis-je exécuter des recherches spécifiques? J'ai entendu parler du modèle de spécification , mais il me semble que cela ne fait que réduire un ensemble complet d'enregistrements (via IsSatisfiedBy() ), ce qui IsSatisfiedBy() clairement des problèmes de performance si vous utilisez une base de données.

Aidez-moi?

Il est clair que j'ai besoin de repenser un peu les choses lorsque je travaille avec des dépôts. Quelqu'un peut-il nous éclairer sur la meilleure façon de le faire?




Ce sont des solutions différentes que j'ai vues. Il y a des avantages et des inconvénients pour chacun d'entre eux, mais c'est à vous de décider.

Issue # 1: Trop de champs

C'est un aspect important surtout lorsque vous prenez en compte les scans indexés seulement . Je vois deux solutions à ce problème. Vous pouvez mettre à jour vos fonctions pour intégrer un paramètre de tableau facultatif qui contiendra une liste de colonnes à renvoyer. Si ce paramètre est vide, toutes les colonnes de la requête sont renvoyées. Cela peut être un peu bizarre; En fonction du paramètre, vous pouvez récupérer un objet ou un tableau. Vous pouvez également dupliquer toutes vos fonctions afin que vous disposiez de deux fonctions distinctes exécutant la même requête, mais l'une renvoie un tableau de colonnes et l'autre renvoie un objet.

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

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

Question n ° 2: Trop de méthodes

J'ai travaillé brièvement avec Propel ORM il y a un an et cela est basé sur ce que je peux me rappeler de cette expérience. Propel a la possibilité de générer sa structure de classe basée sur le schéma de base de données existant. Il crée deux objets pour chaque table. Le premier objet est une longue liste de fonctions d'accès similaire à celle que vous avez actuellement listée; findByAttribute($attribute_value) . L'objet suivant hérite de ce premier objet. Vous pouvez mettre à jour cet objet enfant pour intégrer vos fonctions getter plus complexes.

Une autre solution consisterait à utiliser __call() pour mapper des fonctions non définies sur quelque chose d'exploitable. Votre méthode __call serait capable d'analyser findById et findByName en différentes requêtes.

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

J'espère que cela aide au moins un peu quoi.




Je ne peux que commenter la façon dont nous (mon entreprise) traitons cela. Tout d'abord, la performance n'est pas un problème pour nous, mais le fait d'avoir un code propre / correct est.

Tout d'abord, nous définissons des modèles tels qu'un UserModel qui utilise un ORM pour créer des objets UserEntity . Lorsqu'un UserEntity est chargé à partir d'un modèle, tous les champs sont chargés. Pour les champs référençant des entités étrangères, nous utilisons le modèle étranger approprié pour créer les entités respectives. Pour ces entités, les données seront chargées à nouveau. Maintenant, votre réaction initiale pourrait être ... ??? ... !!! laissez-moi vous donner un exemple d'un exemple:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's 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)
    {
        //...
    }
}

Dans notre cas, $db est un ORM capable de charger des entités. Le modèle indique à l'ORM de charger un ensemble d'entités d'un type spécifique. L'ORM contient un mappage et l'utilise pour injecter tous les champs de cette entité dans l'entité. Pour les champs étrangers, seuls les ID de ces objets sont chargés. Dans ce cas, le OrderModel crée des OrderEntity avec uniquement les ID des ordres référencés. Lorsque PersistentEntity::getField est appelé par OrderEntity l'entité indique à son modèle de charger par chargement tous les champs dans OrderEntity . Tous les OrderEntity associés à un UserEntity sont traités comme un ensemble de résultats et seront chargés en même temps.

La magie ici est que notre modèle et ORM injectent toutes les données dans les entités et que les entités fournissent simplement des fonctions wrapper pour la méthode générique getField fournie par PersistentEntity . Pour résumer, nous chargeons toujours tous les champs, mais les champs référençant une entité étrangère sont chargés si nécessaire. Charger juste un tas de champs n'est pas vraiment un problème de performance. Charger toutes les entités étrangères possibles, mais serait une énorme baisse de performance.

Maintenant sur le chargement d'un ensemble spécifique d'utilisateurs, basé sur une clause where. Nous fournissons un ensemble de classes orientées objet qui vous permettent de spécifier une expression simple qui peut être collée ensemble. Dans l'exemple de code, je l'ai nommé GetOptions . C'est un wrapper pour toutes les options possibles pour une requête select. Il contient une collection de clauses where, une clause group by et tout le reste. Nos clauses where sont assez compliquées mais vous pouvez évidemment faire une version plus simple facilement.

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

Une version la plus simple de ce système serait de passer la partie WHERE de la requête sous forme de chaîne directement au modèle.

Je suis désolé pour cette réponse assez compliquée. J'ai essayé de résumer notre cadre le plus rapidement et le plus clairement possible. Si vous avez d'autres questions n'hésitez pas à les poser et je mettrai à jour ma réponse.

EDIT: De plus, si vous ne voulez pas charger certains champs immédiatement, vous pouvez spécifier une option de chargement paresseux dans votre mapping ORM. Étant donné que tous les champs sont éventuellement chargés via la méthode getField , vous pouvez charger certains champs à la dernière minute lorsque cette méthode est appelée. Ce n'est pas un très gros problème en PHP, mais je ne le recommanderais pas pour d'autres systèmes.




J'ajouterai un peu à cela car j'essaie actuellement de comprendre tout cela moi-même.

# 1 et 2

C'est un endroit parfait pour que votre ORM fasse le gros du travail. Si vous utilisez un modèle qui implémente une sorte de ORM, vous pouvez simplement utiliser ses méthodes pour prendre soin de ces choses. Faites votre propre commandePar les fonctions qui implémentent les méthodes Eloquent si vous en avez besoin. En utilisant Eloquent par exemple:

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

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

Ce que vous semblez rechercher, c'est un ORM. Aucune raison que votre référentiel ne peut pas être basé autour d'un. Cela demanderait à l'utilisateur d'être éloquent, mais personnellement, je ne vois pas cela comme un problème.

Si vous voulez cependant éviter un ORM, vous devrez alors "rouler le vôtre" pour obtenir ce que vous cherchez.

# 3

Les interfaces ne sont pas censées être des exigences strictes et rapides. Quelque chose peut implémenter une interface et y ajouter. Ce qu'il ne peut pas faire est de ne pas implémenter une fonction requise de cette interface. Vous pouvez également étendre les interfaces comme les classes pour garder les choses au sec.

Cela dit, je commence tout juste à comprendre, mais ces réalisations m'ont aidé.




Je pensais que je répondrais à ma propre question. Ce qui suit est juste une façon de résoudre les problèmes 1-3 dans ma question initiale.

Avertissement: Je ne peux pas toujours utiliser les bons termes pour décrire des modèles ou des techniques. Désolé.

Les buts:

  • Créez un exemple complet de contrôleur de base pour afficher et modifier les Users .
  • Tout le code doit être entièrement testable et simulable.
  • Le contrôleur ne devrait avoir aucune idée de l'endroit où les données sont stockées (ce qui signifie qu'il peut être changé).
  • Exemple pour montrer une implémentation SQL (la plus courante).
  • Pour une performance maximale, les contrôleurs ne doivent recevoir que les données dont ils ont besoin, sans champs supplémentaires.
  • La mise en œuvre devrait s'appuyer sur un type de mappeur de données pour faciliter le développement.
  • L'implémentation devrait avoir la capacité d'effectuer des recherches de données complexes.

La solution

Je divise mon interaction de stockage persistant (base de données) en deux catégories: R (Lire) et CUD (Créer, Mettre à jour, Supprimer). Mon expérience a été que les lectures sont vraiment ce qui provoque une application à ralentir. Et tandis que la manipulation de données (CUD) est en réalité plus lente, elle se produit beaucoup moins fréquemment, et est donc beaucoup moins préoccupante.

CUD (Créer, Mettre à jour, Supprimer) est facile. Cela impliquera de travailler avec des models réels, qui sont ensuite transmis à mes Repositories pour la persistance. Notez, mes dépôts fourniront toujours une méthode Read, mais simplement pour la création d'objet, pas pour l'affichage. Plus sur cela plus tard.

R (Lire) n'est pas si facile. Aucun modèle ici, juste valeur des objets . Utilisez des tableaux si vous préférez . Ces objets peuvent représenter un seul modèle ou un mélange de nombreux modèles, rien de vraiment. Ceux-ci ne sont pas très intéressants par eux-mêmes, mais comment ils sont générés. J'utilise ce que j'appelle des Query Objects .

Le code:

Modèle d'utilisateur

Commençons simplement avec notre modèle d'utilisateur de base. Notez qu'il n'y a pas d'extension ORM ou de base de données du tout. Juste la gloire du modèle pur. Ajoutez vos getters, setters, validation, peu importe.

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

Interface de référentiel

Avant de créer mon référentiel d'utilisateurs, je souhaite créer l'interface de mon référentiel. Cela définira le «contrat» que les référentiels doivent suivre pour être utilisé par mon contrôleur. Rappelez-vous que mon contrôleur ne saura pas où les données sont réellement stockées.

Notez que mes dépôts ne contiendront chacun que ces trois méthodes. La méthode save() est responsable à la fois de la création et de la mise à jour des utilisateurs, selon que l'objet utilisateur a ou non un ID.

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

Implémentation du référentiel SQL

Maintenant, pour créer ma mise en œuvre de l'interface. Comme mentionné, mon exemple allait être avec une base de données SQL. Notez l'utilisation d'un mappeur de données pour éviter d'avoir à écrire des requêtes SQL répétitives.

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');
    }
}

Interface d'objet de requête

Maintenant avec CUD (Créer, Mettre à jour, Supprimer) pris en charge par notre référentiel, nous pouvons nous concentrer sur le R (Lire). Les objets de requête sont simplement une encapsulation d'un certain type de logique de recherche de données. Ce ne sont pas des constructeurs de requêtes. En l'extrayant comme notre référentiel, nous pouvons en modifier l'implémentation et le tester plus facilement. Un exemple d'objet de requête peut être une requête AllUsersQuery ou AllActiveUsersQuery , ou même MostCommonUserFirstNames .

Vous pensez peut-être "je ne peux pas simplement créer des méthodes dans mes dépôts pour ces requêtes?" Oui, mais voici pourquoi je ne fais pas ça:

  • Mes référentiels sont destinés à travailler avec des objets de modèle. Dans une application du monde réel, pourquoi aurais-je besoin d'obtenir le champ de password si je cherche à lister tous mes utilisateurs?
  • Les référentiels sont souvent spécifiques à un modèle, mais les requêtes impliquent souvent plus d'un modèle. Dans quel référentiel mettez-vous votre méthode?
  • Cela garde mes dépôts très simples - pas une classe gonflée de méthodes.
  • Toutes les requêtes sont maintenant organisées dans leurs propres classes.
  • Vraiment, à ce stade, les dépôts existent simplement pour résumer ma couche de base de données.

Pour mon exemple, je vais créer un objet de requête à rechercher "AllUsers". Voici l'interface:

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

Implémentation de l'objet de requête

C'est ici que nous pouvons à nouveau utiliser un mappeur de données pour accélérer le développement. Notez que j'autorise un tweak à l'ensemble de données renvoyé: les champs. C'est à peu près tout ce que je veux faire avec la manipulation de la requête effectuée. N'oubliez pas que mes objets de requête ne sont pas des générateurs de requêtes. Ils effectuent simplement une requête spécifique. Cependant, puisque je sais que je vais probablement utiliser celui-ci beaucoup, dans un certain nombre de situations différentes, je me donne la possibilité de spécifier les champs. Je ne veux jamais retourner les champs dont je n'ai pas besoin!

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();
    }
}

Avant de passer au contrôleur, je veux montrer un autre exemple pour illustrer à quel point c'est puissant. Peut-être que j'ai un moteur de reporting et que j'ai besoin de créer un rapport pour AllOverdueAccounts . Cela pourrait être difficile avec mon mappeur de données, et je pourrais écrire du vrai SQL dans cette situation. Pas de problème, voici à quoi pourrait ressembler cet objet de requête:

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

Cela conserve joliment toute ma logique pour ce rapport dans une classe, et il est facile à tester. Je peux me moquer de mon contenu, ou même utiliser une implémentation différente.

Le controlle

Maintenant, la partie amusante - rassemblant toutes les pièces ensemble. Notez que j'utilise l'injection de dépendance. Typiquement, les dépendances sont injectées dans le constructeur, mais je préfère réellement les injecter directement dans mes méthodes de contrôleur (routes). Cela minimise le graphe d'objet du contrôleur, et je le trouve en fait plus lisible. Notez que si vous n'aimez pas cette approche, utilisez simplement la méthode constructeur traditionnelle.

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

Dernières pensées:

Les choses importantes à noter ici sont que lorsque je modifie (créer, mettre à jour ou supprimer) des entités, je travaille avec de vrais objets de modèle et j'effectue la persistance à travers mes dépôts.

Cependant, lorsque je suis en train d'afficher (en sélectionnant les données et en les envoyant aux vues), je ne travaille pas avec des objets de modèle, mais plutôt avec des objets anciens. Je sélectionne uniquement les champs dont j'ai besoin, et il est conçu pour optimiser mes performances de recherche de données.

Mes dépôts restent très propres, et à la place ce "désordre" est organisé dans mes requêtes de modèle.

J'utilise un mappeur de données pour aider au développement, car il est tout simplement ridicule d'écrire du SQL répétitif pour les tâches courantes. Cependant, vous pouvez absolument écrire SQL si nécessaire (requêtes compliquées, rapports, etc.). Et quand vous le faites, il est bien rangé dans une classe correctement nommée.

J'aimerais entendre votre approche de mon approche!

Mise à jour de juillet 2015:

On m'a demandé dans les commentaires où je me suis retrouvé avec tout cela. Eh bien, pas si loin en réalité. Sincèrement, je n'aime toujours pas vraiment les dépôts. Je les trouve exagérément pour les recherches de base (surtout si vous utilisez déjà un ORM), et désordonné lorsque vous travaillez avec des requêtes plus complexes.

Je travaille généralement avec un ORM de style ActiveRecord, donc le plus souvent je vais simplement référencer ces modèles directement dans mon application. Toutefois, dans les situations où j'ai des requêtes plus complexes, j'utiliserai des objets de requête pour les rendre plus réutilisables. Je devrais également noter que j'injecte toujours mes modèles dans mes méthodes, les rendant plus faciles à se moquer dans mes essais.




Merging 2 repos

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master




php database laravel repository repository-pattern