10
\$\begingroup\$

I'm trying to learn the Repository pattern, and I have some questions regarding my current understanding of it.

  1. All the examples I've been able to find of database repositories use ORMs, but for a number of reasons, I can't use an ORM in the project I am learning this for. So, when not using an ORM, where should the SQL queries go? My best guess was in the repository class itself, so that's what I did in the example below.
  2. How's my naming convention for the repository's methods? I stuck with the create/update/delete verbiage of SQL as a sort of placeholder, but is there a better way?
  3. Because I'm not using an ORM, I need a setId() method in my repository. I recognize the danger inherent in allowing id's to be changed after object creation. Right now I prevent that by throwing an exception in setId() if id is not null. Is that alright or is there a better way?
  4. Am I doing anything just completely wrong in general?

Here is my current implementation, as far as I understand the concepts.

Product.php

<?php

namespace Vendor\Package\Module\Entities;

class Product
{
    /** @var int $id */
    protected $id;

    /** @var string $name */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    public function setId($id)
    {
        if ($this->id !== null) {
            throw new Exception('id cannot be reset.');
        }

        $this->id = $id;

        return $this;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }
}

ProductRepositoryInterface.php

<?php

namespace Vendor\Package\Module\Repositories;

use PDO;
use Vendor\Package\Module\Entities\Product;

interface ProductRepositoryInterface
{
    public function findAll();

    public function findById($id);

    public function create(Product $product);

    public function update(Product $product);

    public function delete(Product $product);
}

ProductRepository.php

<?php

namespace Vendor\Package\Module\Repositories;

use PDO;
use Vendor\Package\Module\Entities\Product;

class ProductRepository implements ProductRepositoryInterface
{
    /** @var PDO $db */
    protected $db;

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

    /**
     * @return array
     */
    public function findAll()
    {
        $stmt = $this->db->query(
            'SELECT
                id,
                name
            FROM products
            WHERE active = 1'
        );

        $products = [];
        while ($stmt->fetch(PDO::FETCH_ASSOC)) {
            $product = new Product();
            $product
                ->setId($result['id'])
                ->setName($result['name'])
            ;
        }
        return $products;
    }

    /**
     * @param int $id
     *
     * @return Product
     */
    public function findById($id)
    {
        $stmt = $this->db->prepare(
            'SELECT
                id,
                name
            FROM products
            WHERE id = :id
                AND active = 1
            LIMIT 1'
        );
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        $product = new Product();
        $product
            ->setId($result['id'])
            ->setName($result['name'])
        ;
    }

    /**
     * @param Product $product
     *
     * @return int
     */
    public function create(Product $product)
    {
        $stmt = $this->db->prepare(
            'INSERT INTO products (
                name
            ) VALUES (
                :name
            )'
        );
        $stmt->bindValue(':name', $product->getName(), PDO::PARAM_STR);
        $stmt->execute();

        $id = $this->db->lastInsertId();
        $product->setId($id);
        return $id;
    }

    /**
     * @param Product $product
     *
     * @return bool
     */
    public function update(Product $product)
    {
        $stmt = $this->db->prepare(
            'UPDATE products SET
                name = :name
            WHERE id = :id
                AND active = 1'
        );
        $stmt->bindValue(':name', $product->getName(), PDO::PARAM_STR);
        $stmt->bindValue(':id', $product->getId(), PDO::PARAM_INT);
        return $stmt->execute();
    }

    /**
     * @param Product $product
     *
     * @return bool
     */
    public function delete(Product $product)
    {
        $stmt = $this->db->prepare(
            'UPDATE products SET
                active = 0
            WHERE id = :id
                AND active = 1'
        );
        $stmt->bindValue(':id', $product->getId(), PDO::PARAM_INT);
        return $stmt->execute();
    }
}

demo.php

<?php

use Vendor\Package\Module\Entities\Product;
use Vendor\Package\Module\Repositories\ProductRepository;

$repository = new ProductRepository($db);

// Create
if (
    isset($_POST['create'])
    && isset($_POST['name'])
) {
    $product = new Product();
    $product
        ->setName($_POST['name'])
    ;

    $repository->create($product);
}

// Update
if (
    isset($_POST['update'])
    && isset($_POST['id'])
    && isset($_POST['name'])
) {
    $product = new Product();
    $product
        ->setId($_POST['id'])
        ->setName($_POST['name'])
    ;

    $repository->update($product);
}

// Delete
if (
    isset($_POST['delete'])
    && isset($_POST['id'])
) {
    $product = new Product();
    $product
        ->setId($_POST['id'])
    ;

    $repository->delete($product);
}
\$\endgroup\$

migrated from stackoverflow.com Apr 18 '16 at 19:00

This question came from our site for professional and enthusiast programmers.

2
\$\begingroup\$

A quick answer regarding the setId method.

You should use the object's constructor instead like this:

class Product
{
    /** @var int $id */
    protected $id;

    /** @var string $name */
    protected $name;

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

    public function isNew()
    {
        return is_null($this->id)
    }
}

So if the entity is created for the fist time you have to call $product = new Product() and if you fetch it from the database: $product = new Product($result['id']);.

With this trick you can make a single save operation for your repository:

class ProductRepository implements ProductRepositoryInterface
{

    public function save(Product $product)
    {
        if($product->isNew())
            $this->create($product);
        else
            $this->update($product);
    }
}

For the id question, two options:

  • When you insert a new object, you'll make a new one with the id from the database
  • You'll handle the generation of the Id by yourself using an UUID for example (this lib is perfect for that: https://github.com/ramsey/uuid)

In this second case (you handle the generation of the ID), your class will look like this:

class Product
{
    /** @var int $id */
    protected $id;

    /** @var string $name */
    protected $name;

    protected $isNew = false;

    public function __construct($id = null)
    {
        if($id === null) {
            $this->id = Uuid::uuid4();
            $this->isNew = true;
        } else {
            $this->id = $id;
        }

    }
}

Hope it can help !

\$\endgroup\$
  • \$\begingroup\$ Thank you! I'm not using UUID's (at least not yet), so should save() return the new object when inserting and the given object when updating, to ensure predictable output? And then I'd always need to re-assign "$product = $repository->save($product);" when it isn't known whether it's a new or old Product? \$\endgroup\$ – jpt Apr 19 '16 at 13:27
  • \$\begingroup\$ This is one of the (multiple) reasons that make me uses UUID instead of auto-increment :) : stackoverflow.com/questions/5159413/… \$\endgroup\$ – lilobase Apr 19 '16 at 14:48
  • \$\begingroup\$ But yes, if you want to use db generated id, you'll have to re-assign your product \$\endgroup\$ – lilobase Apr 19 '16 at 14:56
  • \$\begingroup\$ If your database will have 100k+ registers and you need to make joins I would recommend against the use of UUID. The Database engine will index poorly the random numbers. I had a rly bad experience using UUID's in one of my projects :/ \$\endgroup\$ – Kim Aragon Escobar Apr 19 '16 at 17:26
  • \$\begingroup\$ I have the opposite experience, for an invoicing application with massive insert with relation, the capacity to generate our own Id decrease the process time from 7 hours to 7 minutes. And for the index fragmentation (which cause performance issues), you can use sequential UUID. And if the performance is still a problem, you can create a projection of the required datas. Reads can easily be made more efficient, writes are much harder to scale. \$\endgroup\$ – lilobase Apr 19 '16 at 19:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.