- Senior Architect, Palantir.net
- Drupal 8 Web Services Lead
- Drupal Representative, PHP-FIG
- Advisor, Drupal Association
- Loveable pedant
Longest release cycle EVAR!
(Can we please never do that again?)
- 4 year cycle necessary for architecture overhaul
- We don't need overhaul every version
- Unknown length of cycle => uneven plans => longer cycle => uneven plans
The plan
- Day 0: Drupal 8.0.0
- 0+1 month: Drupal 8.0.1 (bug fixes)
- 0+2 month: Drupal 8.0.2 (bug fixes)
- …
- 0+6 month: Drupal 8.1.0 (new features!)
- 0+7 month: Drupal 8.1.1 (bug fixes)
- 0+12 month: Drupal 8.2.0 (new features!)
- 0+13 month: Drupal 8.2.1 (bug fixes)
- …
- Drupal LTS: 8.x.0 (last new features)
- Drupal 9: just API-breaking changes
We don't break APIs in minor versions
(For some definition of API)
Druplicon: API?
An API is a function that performs an operation on a series of arrays.
—Druplicon
WRONG!
(This is why we can't have nice things.)
Old API definition
Any line that begins with "function"
(But not if the function starts with _
(except when that's included too because there's no other way to do something
(When your API definition looks like LISP, you have a problem)
)
)
And arrays. Lots of arrays.
We broke APIs between versions because ourarchitecture didn't let us do otherwise
Now we can do otherwise
We won't break modules in minor versions
—Old BC pledge
We won't break well-behaved modules in minor versions
—New BC pledge
Well-behaved modules
- Uses parts of the code we say are "safe"
- Uses parts of the architecture we say are "safe"
Documentation problem!
Also, "separate safe from unsafe"
Use documentation to drive separation
- Want to be able to change it? Define a safe layer in front of it.
Interfaces
- @api: Safe to type hint, implement, and extend
- @internal: Caveat implementor
- Else: Safe to hint, implement, not extend
Classes
- @api: Safe to type hint, __construct(), protected, public
- @internal: Caveat implementor
- Else: Safe to type hint, __construct(), public, not extend
- Methods may be tagged separately
(We should probably treat traits the same)
We won't break @api and we'll warn you about breaks to normal classes/interfaces
—New BC pledge
There are no hard rules
Only guidelines
Strategic considerations
- Security fixes trump everything
- This will require case-by-case thought
- Subsystem maintainers should be responsible
- Private properties on @api classes?
- @internal: Reserve the right to change, not willy nilly
- Simple pattern application will be insufficient; no rules to shield us from being intelligent
- Subsystem maintainers usually have final say!
General rules
- Database schema is always @internal
- Tests are always @internal
- Controllers are always @internal
- YAML file structure MAY be an API
- Test base classes are like any other class
Tactical guidelines
- "Tagged services": @api interface, tag
- "Tagged service aggregator": @internal class
- Extend classes (*Base): @api with privates
- Break base classes into multiple traits
- Plugins: Mostly @api interface; Manager is @internal
- Traits > Base classes: Some @api, some @internal
- Think through how we want devs to interact (not how they might interact)
- Small surface area is better than large surface area
Container guidelines
- Mark services private (no $container->get())
- Public services must have an interface
- Public service names are @api
Declarative guidelines
- Service tag names: API
- Routing key names: API
- Code used by routing key (_form, _entity_view): @internal
- Config keys: API (unless wrapping service)
- Often the API is behavior, not code. So declare the behavior the API, not the code.
Other guidelines
- 3rd party interfaces: !@api => consider empty extension
- Base class/interface mismatch: Code smell (needs traits?)
- Form arrays: Mostly @internal Except node form?
- Render arrays: Key defs are API, not specific usage
- Base classes example: BlockBase
- Forms: Sneeze and they "break". Need to allow feature addition
Example: Breadcrumbs
- BreadcrumbBuilderInterface: @api
- "breadcrumb_builder" DI tag: @api
- "breadcrumb" service name: @api
- ChainBreadcrumbBuilderInterface: normal
- BreadcrumbManager: @internal
- BreadcrumbBuilderBase: Remove for traits
- BreadcrumbManager: implementation detail
- BreadcrumbBuilderBase = 2 traits. Not needed
@api
Interfaces
- RouteFilterInterface (wrapped)
- RouteEnhancerInterface (wrapped)
- HtmlFragmentInterface
Classes
- RouteSubscriberBase
- ControllerBase
- HtmlFragment
- All exceptions
normal
Interfaces
- UrlGeneratorInterface
- RouteBuilderInterface
- RouteProviderInterface
- ControllerResolverInterface
Classes
- LazyLoadingRouteCollection
- RouteBuildEvent
- UrlGenerator
@internal
Classes
- MatcherDumper et al
- UrlMatcher
- RouteProvider
- CompiledRoute
- Route filters
- Route enhancers
- RouteBuilder
- RouteCompiler
- RoutePreloader
It's the YAML, stupid!
node.content_overview:
path: '/admin/content'
defaults:
_title: 'Content'
_entity_list: 'node'
requirements:
_permission: 'access content overview'
- _title / _title_callback
- _content / _controller
- _form
- _format / method
- _entity_view / _entity_form / _entity_list
Changing what we didn't expect
Rename classes/interfaces
class BookManager {
public function getAllBooks() {}
}
class BookRepository extends BookManager {
public function doSomethingNew() {}
}
function core_cool_stuff(BookRepository $repository) {
$books = $repository->getAllBooks();
$repository->doSomethingNew();
}
function contrib_cool_stuff(BookManager $manager) {
$books = $manager->getAllBooks();
}
Future-looking interfaces
/**
* @api
*/
interface OldInterface {
public function foo();
}
interface NewStyleInterface extends OldStyleInterface {
public function bar();
}
function cool_stuff(OldStyleInterface $service) {
if ($service instanceof NewStyleInterface) {
$bar = $service->bar();
}
else {
$bar = "Some previously assumed value.";
}
// ...
}
When we do that,file a D9 issue to clean it up again
Warning
We are going to screw up andwant to change something we didn't plan for
Learn by failure
We can raise guarantee, never lower.
- Be conservative with @api for now; expand in 8.1, 8.2
We can always raise guarantee, never lower. (So be conservative to start.)
Expectation: We're going to screw up and want to change something we didn't setup for that
Learning experience! We know not to screw it up in D9.
Deadline: 8.0.0.