Is this the common structure for the domain mapper

2020-07-30 00:33发布

问题:

Hopefully i am asking this on the right stack exchange forum. If not please do let me know and I will ask somewhere else. I have also asked on Code Review, but the community seems a lot less active.

As I have self learned PHP and all programming in general, I have only recently found out about 'Data Mappers' which allows data to be passed into classes without said classes knowing where the data comes from. I have read some of the positives of using mappers and why they make it 'easier' to perform upgrades later down the line, however I am really struggling to find out the reccomended way of using mappers and their layouts in a directory structure.

Let's assume we have a simple application whos purpose is to echo out a first name and last name of a user.

The way I have been using/creating mappers (as well as the file structure is as follows):

index.php

include 'classes/usermapper.php';
include 'classes/user.php';

$user = new User;
$userMapper = new userMapper;

try {
  $user->setData([
    $userMapper->fetchData([
      'username'=>'peter1'
    ])
  ]);
} catch (Exception $e) {
  die('Error occurred');
}

if ($user->hasData()) {
  echo $user->fullName();
}

classes/user.php

class User {
  private $_data;

  public function __construct() { }

  public function setData($userObject = null) {
    if (!$userObject) { throw new InvalidArgumentException('No Data Set'); }
    $this->_data = $dataObject;
  }

  public function hasData() {
    return (!$this->_data) ? false : true;
  }

  public function fullName() {
    return ucwords($this->_data->firstname.' '.$this->_data->lastname);
  }
}

classes/usermapper.php

class userMapper {
  private $_db;

  public function __construct() { $this->_db = DB::getInstance(); }

  public function fetchData($where = null) {
    if (!is_array($where)) { 
      throw new InvalidArgumentException('Invalid Params Supplied'); 
    }

    $toFill = null;
    foreach($where as $argument=>$value) {
      $toFill .= $argument.' = '.$value AND ;
    }

    $query = sprintf("SELECT * FROM `users` WHERE %s ", substr(rtrim($toFill), 0, -3));


    $result = $this->_db->query($query); //assume this is just a call to a database which returns the results of the query

    return $result;
  }
}

With understanding that the users table contains a username, first name and last name, and also that a lot of sanitizing checks are missing, why are mappers convenient to use?

This is a very long winded way in getting data, and assuming that users aren't everything, but instead orders, payments, tickets, companies and more all have their corresponding mappers, it seems a waste not to create just one mapper and implement it everywhere in each class. This allows the folder structure to look a whole lot nicer and also means that code isn't repeated as often.

The example mappers looks the same in every case bar the table the data is being pulled from.

Therefore my question is. Is this how data mappers under the 'domain model mappers' should look like, and if not how could my code be improved? Secondly is this model needed in all cases of needing to pull data from a database, regardless of the size of class, or should this model only be used where the user.php class in this case is very large?

Thank you in advance for all help.

回答1:

The Data Mapper completely separates the domain objects from the persistent storage (database) and provides methods that are specific to domain-level operations. Use it to transfer data from the domain to the database and vice versa. Within a method, a database query is usually executed and the result is then mapped (hydrated) to a domain object or a list of domain objects.

Example:

The base class: Mapper.php

abstract class Mapper
{
    protected $db;

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

The file: BookMapper.php

class BookMapper extends Mapper
{
    public function findAll(): array
    {
        $sql = "SELECT id, title, price, book_category_id FROM books;";
        $statement = $this->db->query($sql);

        $items = [];
        while ($row = $statement->fetch()) {
            $items[] = new BookEntity($row);
        }

        return $items;
    }

    public function findByBookCategoryId(int $bookCategoryId): array
    {
        $sql = "SELECT id, title, price, book_category_id 
                FROM books
                WHERE book_category_id = :book_category_id;";

        $statement = $this->db->prepare($sql);
        $statement->execute(["book_category_id" => $bookCategoryId]);

        $items = [];
        while ($row = $statement->fetch()) {
            $items[] = new BookEntity($row);
        }

        return $items;
    }

    /**
     * Get one Book by its ID
     *
     * @param int $bookId The ID of the book
     * @return BookEntity The book
     * @throws RuntimeException
     */
    public function getById(int $bookId): BookEntity
    {
        $sql = "SELECT id, title, price, book_category_id FROM books 
                WHERE id = :id;";

        $statement = $this->db->prepare($sql);

        if (!$result = $statement->execute(["id" => $bookId])) {
            throw new DomainException(sprintf('Book-ID not found: %s', $bookId));
        }

        return new BookEntity($statement->fetch());
    }

    public function insert(BookEntity $book): int
    {
        $sql = "INSERT INTO books SET title=:title, price=:price, book_category_id=:book_category_id";
        $statement = $this->db->prepare($sql);

        $result = $statement->execute([
            'title' => $book->getTitle(),
            'price' => $book->getPrice(),
            'book_category_id' => $book->getBookCategoryId(),
        ]);

        if (!$result) {
            throw new RuntimeException('Could not save record');
        }

        return (int)$this->db->lastInsertId();
    }
}

The file: BookEntity.php

class BookEntity
{
    /** @var int|null */
    protected $id;

    /** @var string|null */
    protected $title;

    /** @var float|null */
    protected $price;

    /** @var int|null */
    protected $bookCategoryId;

    /**
     * Accept an array of data matching properties of this class
     * and create the class
     *
     * @param array|null $data The data to use to create
     */
    public function __construct(array $data = null)
    {
        // Hydration (manually)
        if (isset($data['id'])) {
            $this->setId($data['id']);
        }
        if (isset($data['title'])) {
            $this->setTitle($data['title']);
        }
        if (isset($data['price'])) {
            $this->setPrice($data['price']);
        }
        if (isset($data['book_category_id'])) {
            $this->setBookCategoryId($data['book_category_id']);
        }
    }

    /**
     * Get Id.
     *
     * @return int|null
     */
    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * Set Id.
     *
     * @param int|null $id
     * @return void
     */
    public function setId(?int $id): void
    {
        $this->id = $id;
    }

    /**
     * Get Title.
     *
     * @return null|string
     */
    public function getTitle(): ?string
    {
        return $this->title;
    }

    /**
     * Set Title.
     *
     * @param null|string $title
     * @return void
     */
    public function setTitle(?string $title): void
    {
        $this->title = $title;
    }

    /**
     * Get Price.
     *
     * @return float|null
     */
    public function getPrice(): ?float
    {
        return $this->price;
    }

    /**
     * Set Price.
     *
     * @param float|null $price
     * @return void
     */
    public function setPrice(?float $price): void
    {
        $this->price = $price;
    }

    /**
     * Get BookCategoryId.
     *
     * @return int|null
     */
    public function getBookCategoryId(): ?int
    {
        return $this->bookCategoryId;
    }

    /**
     * Set BookCategoryId.
     *
     * @param int|null $bookCategoryId
     * @return void
     */
    public function setBookCategoryId(?int $bookCategoryId): void
    {
        $this->bookCategoryId = $bookCategoryId;
    }
}

The file: BookCategoryEntity.php

class BookCategoryEntity
{
    const FANTASY = 1;
    const ADVENTURE = 2;
    const COMEDY = 3;

    // here you can add the setter and getter methods
}

The table structure: schema.sql

CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `price` decimal(19,2) DEFAULT NULL,
  `book_category_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `book_category_id` (`book_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `book_categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `book_categories` */

insert  into `book_categories`(`id`,`title`) values (1,'Fantasy');
insert  into `book_categories`(`id`,`title`) values (2,'Adventure');
insert  into `book_categories`(`id`,`title`) values (3,'Comedy');

Usage

// Create the database connection
$host = '127.0.0.1';
$dbname = 'test';
$username = 'root';
$password = '';
$charset = 'utf8';
$collate = 'utf8_unicode_ci';
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_PERSISTENT => false,
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES $charset COLLATE $collate"
];

$db = new PDO($dsn, $username, $password, $options);

// Create the data mapper instance
$bookMapper = new BookMapper($db);

// Create a new book entity
$book = new BookEntity();
$book->setTitle('Harry Potter');
$book->setPrice(29.99);
$book->setBookCategoryId(BookCategoryEntity::FANTASY);

// Insert the book entity
$bookId = $bookMapper->insert($book);

// Get the saved book
$newBook = $bookMapper->getById($bookId);
var_dump($newBook);

// Find all fantasy books
$fantasyBooks = $bookMapper->findByBookCategoryId(BookCategoryEntity::FANTASY);
var_dump($fantasyBooks);