If I understood your point ...
Having all your concrete mappers inheriting SQL from a common class has several issues that you have missed:
- parameter names in your domain objects depend on the names of columns
- there is a "fetching method" in mappers, that don't have a corresponding table
- you still have configuration (table name), that is expected by superclass
- the DB schema must have
id
as name for all of your PRIMARY KEY
columns
Now, I'm gonna try to unpack each of those.
Parameter and column names
To create a shared findById()
method, the only pragmatic approach is to build it around something like this:
"SELECT * FROM {$this->tableName} WHERE id = :id"
The main issue actually is the wildcard *
symbol.
There are two major approaches for populating an entity using a data mapper: use setters or use reflection. In both cases the "names" of a parameters/setters is implied by columns, that you have selected.
In a normal query you can do something like SELECT name AS fullName FROM ...
, which lets you to use the query for re-naming the fields. But with a "unified approach", there are no good options.
Each mapper can fetch data by id
?
So, the thing is, unless you have a mapper-per-table structure (in which case an active record starts look like pragmatic option), you will end up with few (really common) "edge case" scenarios for your mappers:
- only used to save data
- deals with collection and not singular entities
- aggregates data from multiple tables
- works with a table, that has composite key
- it's actually not a table, but an SQL view
- ... or combination of the above
Your original idea would work just fine in a small scale project (with one or two mappers being an "edge case"). But with a large project, the usage of findById()
will be the exception not the norm.
Independent parenting?
To actually get this findById()
method in the superclass, you will need a way to communicate the table name to it. Which would mean, that you have something like protected $tableName
in you class definition.
You can mitigate it by having abstract function getTableName()
in your abstract mapper class, which, when implemented, returns a value of global constant.
But what happens, when your mapper need to work with multiple tables.
It seems like a code smell to me, because information actually crosses two boundaries (for lack of better word). When this code breaks, the error will be shown for SQL in the superclass, which isn't where the error originated from (especially, if you go with constants).
Naming the primary key
This is a bit more controversial opinion :)
As far as I can tell, the practice of calling all primary columns id
comes from various ORMs. The penalty, that this incurs, applies only to readability (and code maintenance). Consider these two queries:
SELECT ar.id, ac.id
FROM Articles AS ar LEFT JOIN
Accounts AS ac ON ac.id = ar.account_id
WHERE ar.status = 'published'
SELECT ar.article_id, ac.account_id
FROM Articles AS ar LEFT JOIN
Accounts AS ac USING(account_id)
WHERE ar.status = 'published'
As the DB schema grows and the queries become more complex, it gets harder and harder to actually keep track of, what the "id" stands for in what case.
My recommendation would be to try same name for column, when it is a primary as when it is a foreign key (when possible, because in some cases, like for "closure tables, it's not viable). Basically, all columns that store IDs of same type, should have the same name.
As a minor bonus, you get the USING()
syntax sugar.
TL;DR
Bad idea. You are basically breaking LSP.