Structure of a craft app¶
A craft app is made up of several components and objects. This page explains the relationships and communication between these components.
Here’s a view of the structure of a craft app, starting from when a user calls it:
Relationship between pieces of an app.¶
The app is invoked by an external process that runs the app with some command.
This instantiates an Application
class and runs its run method, which configures the
ServiceFactory
instance before the Command instance’s run()
method. A
Command
may get any necessary services from the ServiceFactory
using its
get
method, and it may make any service-specific calls to any service it gets.
Likewise, a service may interact with another service’s public API by using
ServiceFactory.get()
to get another service. Services may store data in Model
instances. When a service uses a Model
to store data, that service is responsible
for maintaining that model and providing access either to the model or its underlying
features.
Application
¶
The Application
class is the glue that
holds a craft app together. The Application
is primarily responsible for starting
up the app, loading the relevant services and calling the relevant Command class.
It also handles exceptions and turns them into error messages.
In an ideal world, the Application
class would never need to have child
classes. Each thing the Application
class currently does that requires an override
in a craft can be considered a shortcoming to be fixed at a later date. In practice,
however, this is not always feasible.
Commands¶
A Command
class is what’s responsible for user interaction. It is a child class of
craft_application.commands.AppCommand
that gets registered with the Application
class by using its
add_command_group()
method.
It should:
Take parameters for every option, which can be set interactively (i.e. from the CLI) or non-interactively (i.e. from a script).
If an option is not provided, ask the user or provide a reasonable default.
Return final information to the user.
To do all of this, it is supported by Craft CLI,
the application’s metadata, and the
ServiceFactory
class. The ServiceFactory
instance
is accessible at self._services
, while the app’s metadata is accessible at
self._app
.
Interactive functionality should generally be implemented within a Command class.
One such example is the remote-build
command, which asks the user to confirm
that they are okay uploading the project to a public git repository. In addition to
that interactive item, it provides a way to do this non-interactively via
--launchpad-accept-public-upload
. It is supported by a remote build service that
provides the actual business logic, but the user interface is primarily driven
by the Command.
Services¶
Services are where an app’s business logic lives. They are responsible for:
Implementing logic related to a specific workflow or piece of data.
Maintaining relevant internal state for that (assisted by the
StateService
if the state needs to be passed between application instances running inside and outside of an isolated build environment, respectively referred to as managed and manager instances).Acting as wrappers for any external libraries that get used (with a few exceptions).
Caution
Avoid storing data from one service in another. Services may update data by replacing an existing object rather than mutating it. Unless stated otherwise, each call to a service should be treated as invalidating any cached items retrieved from that service.
A prime example of this is the ProjectService, which is the sole
provider of information about the loaded project. The contract of the ProjectService
is such that an instance of a project model may be disposed and replaced, so a command
or another service must not keep a project model instance, but instead request the
project through the ProjectService
in each method or after each call that may
modify the project model (for example,
deep_update()
).
The ProjectService
is also responsible for loading the project file, parsing
the YAML, performing pre-processing on it, and rendering a
Project model. Additional project-related features should be
implemented by extending this service, not with ad-hoc logic in other places.
A service that implements workflow-related logic and acts as a wrapper for an external
library is the ProviderService
, which
provides relevant hooks into Craft Providers. This service
contains both global state (e.g. packages and snaps to install in a managed instance)
and per-instance state (e.g. the actual instance
from Craft Providers).
Services may interact with each other, but it is strongly recommended that they only do so at a high level in order to avoid too much complexity. This includes the fact that services should default to using protected (underscore-prefixed) methods unless it is specifically known that a method must be externally available. Likewise, when possible a command should only interact with a service on a high level.
Models¶
Pydantic models validate data when reading and writing files. A
CraftBaseModel
is available to create a
model that has convenience methods that are commonly used by Craft apps. The only
logic in a model should ideally be its validation and serialization logic.