It would be sweet if we had a nicer syntax for hook_schema.
Imo, if we do it right, then shorter = easier to read and write.
D8:
Whether we move this to an event, is a different question, and independent of this proposal.
EDIT (donquixote):
I originally had two ideas about this, but find one a lot more convincing. As so far I am the only one active on this issue, I take the freedom to remove the other proposal.
I recently did some experiments in D7, and this is the (working) result: http://drupal.org/project/injapi
The current proposal is mostly taken from there.
Method chaining / "fluid" syntax for hook_schema().
The hook_schema() would receive an argument that would be the starting point for the method chaining game.
(I say "$api" for "InjectedAPI", following a pattern I use in my contrib projects. Feel free to suggest otherwise.)
The draft is taken directly from injapi module:
http://drupalcode.org/project/injapi.git/blob/refs/heads/7.x-1.x:/exampl...
/**
* Implements hook_schema()
*/
function hook_schema($schema) {
// That's the table.
$table = $schema->table('comment');
// Primary key.
// "field_id" automatically gives us auto-increment, and registers this field as the primary key.
$table->field_id('cid');
// Foreign keys
// field_idForeign can take optional parameters to automatically create relations.
$table->field_idForeign('pid');
$table->field_idForeign('nid', 'node', 'nid', 'comment_node');
$table->field_idForeign('uid', 'users', 'uid', 'comment_author');
// Data fields
// Most field types have extra parameters for stuff that is special to this type. E.g. varchar length.
// required() has a second parameter for the default value.
$table->field_varchar('subject', 64)->required(TRUE, '');
$table->field_varchar('hostname', 128)->required(TRUE, '');
$table->field_seconds('created')->required(TRUE, 0);
$table->field_seconds('changed')->required(TRUE, 0);
$table->field_unsigned('status', 'tiny')->required(TRUE, 1);
$table->field_varchar('thread', 255)->required();
$table->field_varchar('name', 60);
$table->field_varchar('mail', 64);
$table->field_varchar('homepage');
$table->field_varchar('language', 12)->required(TRUE, '');
// Field descriptions
// We keep these separate, to have the upper part more readable.
// This said, we could instead just put the ->description() on each field directly.
$table->describeFields(array(
// Id columns
'cid' => 'Primary Key: Unique comment ID.',
'pid' => 'The {comment}.cid to which this comment is a reply. If set to 0, this comment is not a reply to an existing comment.',
'nid' => 'The {node}.nid to which this comment is a reply.',
'uid' => 'The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.',
// Data columns
'subject' => 'The comment title.',
'hostname' => "The author's host name.",
'created' => 'The time that the comment was created, as a Unix timestamp.',
'changed' => 'The time that the comment was last edited, as a Unix timestamp.',
'status' => 'The published status of a comment. (0 = Not Published, 1 = Published)',
'thread' => "The vancode representation of the comment's place in a thread.",
'name' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.",
'mail' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
'homepage' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
'language' => 'The {languages}.language of this comment.',
));
// Indexes
$table->index('comment_status_pid', array('pid', 'status'));
$table->index('comment_num_new', array('nid', 'status', 'created', 'cid', 'thread'));
$table->index('comment_uid', array('uid'));
$table->index('comment_nid_language', array('nid', 'language'));
$table->index('comment_created', array('created'));
}
Implementation
The implementation can be copied and adapted directly from injapi module.
The objects ($schema, $table, $field) used in here are just temporary for the purpose of this hook, and will not be used anywhere else in the db engine.
The $schema object might be created new for every invocation, or we use the same all over.
The result of a hook invocation will still be a huge array, after all is done.
Benefits
- Nicer to read (imo).
- Faster to type.
- Less scrolling.
- More visual structure in the code.
- Easier to focus on the intention of things, rather than the technical details.
- Well-known patterns such as "auto-increment id" are shortcutted, so you don't have to guess that from the values.
- Possibility for immediate sanity checks in the various methods, so that the array structure collected in $schema can be always sane.
Cons
- A tiny overhead, but that doesn't matter, because it is only on rebuild.
- No idea if we have anything other planned for hook_schema().
- Not sure what to do with hook_schema_alter().
Thoughts
Memory concerns?
We still have the option to run this thing separately per module, with a new $schema object per invocation. I don't think this is actually necessary, but the option exists.
Comments
Comment #1
donquixote commentedUpdate:
In my own projects I went to calling this pattern "Injected API". The argument is then always named "$api". I am going to change the issue description accordingly.
Comment #2
donquixote commentedThis can be seen in action here:
http://drupalcode.org/project/injapi.git/blob/refs/heads/7.x-1.x:/exampl...
(and changing the issue description, again)
Comment #2.0
donquixote commentedLink to a more recent example / draft, with a slightly different syntax.
Comment #2.1
donquixote commentedUpdated issue summary to describe the latest proposal, and not distract with details we are going to discard anyway.
Comment #2.2
donquixote commentedUpdated issue summary.
Comment #2.3
donquixote commentedUpdated issue summary.
Comment #3
donquixote commentedAn alternative would be to move to yml files, like with routes.
(far away from saying it should be CMI !!)
That would still be more verbose than what is suggested above, but nicer to type than PHP arrays..
Comment #3.0
donquixote commentedReplacing "$h" with "$api"
Comment #4
donquixote commentedUpdated summary, now we also update the title.
Comment #5
donquixote commentedRelated:
#432440: Update Schema API to make it more usable
I should mention that this other issue is trying to solve a different problem, and has nothing to do with the syntax within hook_schema().
This issue here, on the other hand, is only about DX syntax candy within hook_schema(), nothing more.
It does not plan to change the internal array structure.
Comment #6
Crell commentedI actually rather detest hook_schema in its entirety. I'd rather just kill it in favor of something cleaner and less Drupal-specific, per the linked issue. Adding utility routines inside hook_schema is just putting lipstick on a pig.
Comment #7
donquixote commentedI thought the linked issue was more thought for the internal implementation of "writing the schema stuff to the db", and for hook_update implementations.
How does the alternative look like? Crud?
That's the same controversy again as we have with CMI for routes: persistent state + crud, vs info + cache. Imo, whatever we do with state + crud, it always means that modules need to do a lot of babysitting, which I would prefer them to not have to care about.
This said, I don't mind moving towards events or yml.
I wonder what other systems do, I did not do any research on that.
Comment #8
donquixote commentedThough yes, I know doctrine. I just think we are far away from that, and the particular implications of that would not fit for us.
Comment #9
donquixote commentedAlso, don't we have enough bloodshed in D8 already.. shouldn't we leave some killing for D9?
The hook_schema and "huge array of definitions" (not talking about implementation) as is doesn't make realworld problems afaik, and this particular change is totally harmless and can even be backwards compatible.
Comment #10
donquixote commentedAnd yes, this is all about lipstick. Very nice lipstick.And then again, it's not. It's giving a new face to the pig. And then at some day replacing the pig behind, if we feel that way.
Comment #11
Crell commentedIf we're going to come up with a new syntax we expect everyone to use inside hook_schema(), then it seems there's little point to it unless we also make sure it's a syntax we intend to be usable long-term. And then, it's only a short skip to removing hook_schema() in favor of some non-hook way of defining tables. (Since the hook, like so often, introduces some weird dependencies at times.)
My thought from the other issue (or after a year of further thinking) is that we need to, first, provide an introspection system for the DB layer to tell us what the state of a table is. That involves defining some table object that represents the introspected data, which we can then retrieve at any time. (schema.module in contrib already has the MySQL code we'd need, it just needs to get OOPified.) Then once that class is defined, declaring a table becomes a matter of a module providing such a class, and the DB layer can round-trip it. ($table = new MyModuleTable(); $conn->schema->createTable($table);) DDL queries are then query builders in the same vein as our other query builders, chained like everything else.
Make sense? (I don't know how much of that is feature-freeze material, and I've been kinda busy with WSCCI which is why I've not gotten back to it.)
Comment #12
donquixote commentedThanks for picking this up!
That would be, e.g. a table structure depends on the definition of a content type, a field, etc?
I don't see this as a problem necessarily.
You need to be aware of the dependencies when you design a module, and then design a clearly defined food chain. If you ignore that, you get into trouble. But those dependencies do not exist because of the hook, but because of what you want to achieve.
But maybe you mean sth else?
I think Zend does it that way (or did, when I used that ages ago).
Totally doesn't help with dynamic schema definitions - unless you play with code generation.
I always found that boilerplatey and stupid, writing those classes.
And rightfully, Doctrine lets you write yaml files instead, which lets you focus on the actual definition of the table, instead of the boilerplate getters, setters and friends. This yaml feels very much like our hook_schema(), with some additions.
From there you get those classes via code generation, and you subclass them to implement custom behavior.
For Drupal I don't really believe in "one class per file" approach:
- It does not help for dynamic schema definitions (e.g. for content types)
- It does not allow multiple modules to participate in the schema definition. No, I don't count subclassing as a solution, that is too one-dimensional.
- It creates a lot of duplicate and meaningless code.
I'd rather see
- some code to produce table definitions (hook_schema, yaml, etc)
- operations that are used internally to create those tables in the db.
- generic classes for tables and fields/columns, which are instantiated with specific configuration, instead of being subclassed. Yes, this would give you the introspection you want.
- reusable "behaviors" which can be attached to tables or columns.
- a query builder similar to dbtng (or just keep that)
Actually I don't really care about the other points.
The main thing is, as a module developer I want a pleasant way to define my schema, without doing anything redundant or bureaucratic. What the system does with that, is not part of this issue: It can either just remember the schema somewhere, or if you want, generate those classes as class-per-table, like Doctrine does.
If we compare
- Doctrine-style yaml files (you can also have XML, but seriously who wants that :) ) -> static
- Doctrine-style yaml + php (I think I saw that somewhere)
- hook_schema() + alter with nested arrays
- hook_schema() + alter with method chaining, as suggested here
- a hybrid hook_schema() which allows nested array return value as well as method chaining.
- the same, but moving that to schema builder events, schema factories, or the like.
Some details aside, these are all exchangeable. Except
- some work better than others for dynamic schema definitions
- there's this old "NIH" argument.
- Doctrine afaik has some additional keys to attach behaviors. This is pretty cool, but nothing stops us from doing the same.
- hook_schema + alter is better at letting multiple modules participate in the definition process.
We can easily have our introspection objects today, with a few classes instantiated with existing hook_schema definitions.
I can imagine very little that this object could tell us, where we would absolutely need a "class per table" approach. Neither generated nor hand-written.
And due to our modularity, we are unlikely to hardcode any business logic into those classes.
Comment #13
donquixote commentedBtw,
there is this issue #1446808: Let plugin managers announce changed definitions
and this post in gdo suggesting a "dependency matrix": http://groups.drupal.org/node/267163#comment-850373
(yes, you probably know that already)
Comment #14
Crell commentedActually I was referring to the bootstrap knot of doom that hooks result in. Plus the lazy load questions around specialty hooks.
I am not suggesting forcing a class name per table. I'm suggesting wrapping the information about a table into a typed object, which is then bidirectional.
Dynamic schemas are fine with that, because they can create and populate a Table object whenever they like and pass that in. But they can also then introspect tables, too.
Even if we offered a YAML file for module devs as well (wait, I thought you didn't like YAML for Routes? :-) ), we'd still need such an in-PHP representation of a table anyway because we are *not* baking a YAML dependency into the database system. It is actually rather analagous to Routes (PHP object for the definition, ability to dump out to or read in from whatever other serialized formats you feel like writing as a DX convenience). That PHP object has to come first, though.
changeTable() I see going away in favor of a query builder that feels a lot like the insert/update/delete builders.
Comment #15
donquixote commentedI don't see that problem, esp with hook_schema(). If you could explain .. :)
The only issue I could see is, you are creating a content type, and instead of doing the schema change directly, you need to wait for a "schema rebuild" to do that for you? Not sure if that is even the case today..
yaml on itself is great.
The issue I have is yaml for static + something else for dynamic + yet something else for altering, that is 3 different "languages" to learn. With nested definition arrays, you learn the static definition first, and then the rest is just obvious. You can open a random *.install file and look for hook_schema(), and you know it all.
We might get used to it if this becomes a common pattern.
This being said: yaml + alter + dynamic, original hook_schema, "fluid" hook_schema, they are all in the same boat if compared to what you seem to be proposing .. so I'd like to get over those basic decisions first, before we go back to DX bikeshed (which this issue was originally about)
Yes, sure.
Not sure what else you want to do with the introspection table object.
My point is, the average module should not be writing a MyTable class, but just provide its definition, so core can just instantiate the generic Table class.
The code above is using the table as an "ActiveRecord". I'm not sure yet that is a good idea.
I always had the idea that the representation of an object as-it-is-now should be separate from the object as-i-want-to-modify-it. Similar to that the representation of an entity-in-the-db should be separate from entity as-i-want-to-render-it.
I think this depends what else we want to use the table object for.
Comment #16
Crell commentedAs you see in the other issue, we were rather stalled on the question of builders vs. state objects for DDL queries. (ie, db_change_table('tablename')->addColumn(...)->execute() vs. the load/save example you have.) There's decent arguments both ways, and we never came to a conclusion on that by the time D7 hit freeze. Since then I've decided my preference is for create table and "what is this table" to be state objects, but change table to be a builder. That way we avoid having to deal with "here's two objects, magically figure out how to turn one into the other", which is a complication I don't want.
drupal_get_schema() had some horrible knots in early bootstrap vis a vis module_list(), the value of which changed depending on when in bootstrap you called it because it cached values differently depending on how it was last called. It was seriously broken steaming pile. That has fortunately been largely cleaned up now in D8 (thanks, chx!), but hook_schema, as a hook, still depends on modules. I do not like the idea of the DB routines being dependent on modules, and on at least some drivers certain query builders are dependent (or were at one point dependent) on the schema, which in turn makes those drivers dependent on a fully running Drupal instance, which is doubleplusungood. Allowing the drivers to introspect themselves and learn what the schema is when needed keeps things nicely decoupled. Once you have that, defining new tables using the same data object is a no-brainer.
That's basically the logic that led me to my current position.
Comment #17
donquixote commentedNice, I think I'm getting more of a bigger picture now :)
I think the solution is to properly slice the responsibilities and dependencies.
Here is how I imagine it could work:
Module schema definitions:
hook_schema(), yaml or similar (+ alter) tell us how modules want the schema to be like.
This has to depend on modules naturally. But we only use it during "rebuild time".
Stored schema definitions:
In addition, we have to remember how the schema is right now, independent of how modules want it to be. This information can be independent of modules, and available at runtime early in the request.
"remember" can mean CMI or any persistent storage we have available. It could be remembered in the database, but only if we manage to "protect this from itself" (so probably better not).
This schema information will have the basic table definitions, plus any meta info that we like.
Active Record:
$table->save()"active record" writes a schema definition both to the schema storage, and runs the queries to actually create or modify the tables in the database.This can be used any time to quickly change a schema, at the developer's own risk. E.g. after creating a field, we could just directly say $schema->createTable('my_field')->addColumn(...)->save().
And it will be used in core, on "rebuild time", to bring the schema back to the structure intended and described by modules.
Note: This may conflict with hook_schema(), e.g. if hook_schema_alter() adds an additional column, that our module does not know about. See below.
Query engine:
db_change_table('tablename')->addColumn(..)->execute() is just a query builder engine which only talks to the database, but has no idea of schema definitions within Drupal.
We could use that within the implementation of the Table "active record", but probably no other modules will use it.
Whether we actually need this thing, I dunno.
Schema rebuild:
"Rebuild" means that the schema will be reset to the state that modules intend (via yaml, hook_schema + alter, etc). Any previous changes done with direct calls to $table->save() will be overridden by the information from hook_schema(). So if the field module creates a table, and on the next rebuild it doesn't know about that table anymore, the table will be wiped (*).
This gives us a reliable way to clean up a messed-up database structure and turn back to the structure we expect.
(*) We need a way to avoid unintended data loss on those rebuilds. We could say that harmful operations are not executed, and a warning + diff is sent instead - so the module developer has a chance to resolve it in hook_update_N(). (I didn't really check how we do that today, might be we already do sth smart there?)
Syntax on $table and in hook_schema():
We could let hook_schema() and the active record thing use the same table class, or at least methods that behave similarly.
On the other hand, I would like hook_schema to be purely declarative. Any ->save() should not be executed within there.
I think we should postpone that until after we have a plan for the other things.
3rd party code?
A lot of this sounds like we could find it in 3rd party code. Otherwise, we could code that as an independent library and then bring it back to Drupal :)
DISCLAIMER:
I have not really studied the other issue yet, so some of that might change my mind.
Comment #18
donquixote commentedBtw, instead of letting modules use $table->save(), we could also allow them to request a partial schema rebuild - e.g. only for the module's own tables. Not sure if this could bring us any trouble, e.g. if a table "changes its owner" from module 1 to module 2.
Comment #19
Crell commentedThe "Rebuild" process is the one that I don't think is reasonable. That requires an awful lot of involved logic that has to know about specific details of certain database engines and what order certain operations can happen in, what operation combinations cannot happen together, etc. There's also potential data loss situations when fields change their type, which really ought to be handled manually. I'm not convinced that's feasible in practice.
The builder-vs-AR-style question is exactly what the other issue was about. I think we've about rehashed that whole issue by now and should probably merge them. :-)
FTR: I don't think hook_schema_alter() should be allowed to exist. It's a heisenbug waiting to happen.
Also consider that in Drupal 8, between Entity API and CMI, I expect the number of DB tables that modules define should drop dramatically. We may be over-engineering this question for something that will be of declining use. We're also now well outside the realm of what can be done in the next 2 weeks, esp. with most people focused on the main initiatives, and I doubt this level of rebuild is viable post-freeze.
Comment #20
donquixote commentedThere seems to be the general controversy about "collect info and rebuild" vs "allow modules to do crud operations on an existing structure". (or do both)
The D7 equivalent is hook_install() / hook_schema() on the one hand, and hook_update_N() on the other hand. (and in this case you need both)
hook_schema() means I describe how I want the table structure to look like right now.
hook_update_N() means I describe the operations to get from the current structure to the structure that I want.
If I understand you correctly, you propose to ditch hook_schema() entirely, so that in hook_install() I would have to do operations very similar to hook_update_N(). (only that I would know that none of those tables exist yet).
As a module developer, I enjoy writing and modifying hook_schema(), but I hate writing hook_update_N().
So, I would like to propose something to turn this around :)
This seems to me like an awesome-DX solution.
(*) I say hook_schema() and hook_update_N() all the time, but this could be replaced by something else - event subscriber classes, yaml, whatever.
EDIT:
This does not account for dynamic schema changes outside of module updates, or hook_schema() depending on dynamic data. In this case, the developer needs to do more stuff manually in hook_update_N().
Comment #21
Crell commentedI think we're talking at 2 different levels.
The database system itself, independent of Drupal in any way, needs to have the following:
1) A way to extract a table definition object from an actual database.
2) A way to take that table definition object and create the table it represents, round-tripping point 1.
3) A way to modify a table in-place (that is more robust than our current one-change-at-a-time implementation).
Those are core features of the database system that it needs, unto itself, completely in isolation of the rest of Drupal.
Once the database layer is so-enhanced, then we can build other things on top of that as you mention. Such things include:
- Shipping a table definition object with a module (by whatever mechanism, class or YAML).
- Update hooks that contain various modify-in-place commands, as now.
- Some sort of magical "table structure diff" that takes 2 table objects and generates the appropriate modification command object. (This may get pushed down to the DB system itself, but it depends on the stuff above.)
- Caching of the table structure information. (Although if the FIG group ever defines a standardized caching interface, I am totally open to baking that into the DB layer directly.)
I fully support adding all of that functionality, but that is a phase 2 and must come after the changes above.
The most crucial change is the source of authority. Drupal currently assumes that the module-defined schema is definitive, and if the database itself doesn't match up then oh-well, things explode. That's backwards. Instead, the database itself should be definitive, and parts of Drupal that rely on database structure for their logic should get that information from the database, not from what the module developer claims the database is supposed to be.
There are 2 reasons for that. One, it's simply more correct conceptually. Two, Drupal's schema system requires a fully booted system: Hooks must work, caching must work, and there is, or was, a rather weird dependency on bootstrap phases due to module_list() being a knot of evil. (I don't know if that's been fixed yet in Drupal 8. I hope so.) I want to eliminate any and all such dependencies. If you can get a database connection, you should be able to get an up-to-date schema definition for a table without depending on any other part of Drupal whatsoever. That's good loose coupling, which is the direction Drupal has been, slowly and with much wailing and gnashing of teeth, moving. :-)
Comment #22
donquixote commentedOk, I think we are not so far apart.
I always understood (and I think I mentioned that) that this issue is about your "phase 2", whereas the other issue is about the phase 1 stuff.
A different way to see it, is that this issue is where we have a private conversation, whereas the crowd is in the other issue :)
I have no objections with either of these viewpoints :)
I agree with your phase 1.
I still like my fluid syntax, but I agree we should do the low-level API first, and anyway it's all D9. Until then I will simply use my personal InjAPI bicycle, because I hate those huge arrays.
Yep.
I just wonder if we should have a 3rd level in between, where we remember the schema information on every change. This might be a simple cache, but it could also remember some meta information that we cannot simply extract from the tables themselves, or simply, a more semantic and db-engine-independent representation of the schema.
Yep.
The authoritative description of the current state of the database can and should be available on early bootstrap.
The "how modules claim or want the schema to be like" can wait until much later in the request.
Comment #23
donquixote commentedBtw, isn't my post in #17 already quite close to that?
(I am missing some points there, but the main idea of separating current schema from the intended schema is there)
Comment #24
donquixote commentedThe "whatever mechanism" was the original intention of this issue.
I see this has to wait, but I would like to make a point, while we are at it.
I have seen some frameworks where you have a "one class per table" approach. This class contains one method to define the columns, and then some methods for CRUD operations.
I always found this approach kinda stupid. Even with code generation, you end up looking at lots of redundant code. Or, if the table contains only the definition method, then you have plenty of classes that do nothing.
I would agree with tables being objects, but if all they have is the definition of columns, then I'd rather see them as instances of a generic class, with a configuration argument.
Also I don't think that the CRUD operations, if any, need to live on the same object/class as the schema definitions.
----------
The "Shipping a table definition" is most relevant for modules that have a static schema, not depending on any other state information. Here the schema definition (by "whatever mechanism") is the developer's main point of schema control.
For modules with a dynamic schema, we may still have a (dynamic) table definition, but here it would be used more for testing than for anything else.
Comment #25
donquixote commented(re-add tag)
Comment #25.0
donquixote commentedAnswer the expected comment about memory.
Comment #39
smustgrave commentedThank you for sharing your idea for improving Drupal.
We are working to decide if this proposal meets the Criteria for evaluating proposed changes. There hasn't been any discussion here for 8+ years which suggests that this has either been implemented or there is no community support. Your thoughts on this will allow a decision to be made.
Since we need more information to move forward with this issue, the status is now Postponed (maintainer needs more info). If we don't receive additional information to help with the issue, it may be closed after three months.
Thanks!
Comment #40
smustgrave commented3 months has past so closing out, but don't worry if still a valid task this can be re-opened by anyone.
Thanks!