Spec: Ploi Provider
Issue: MAR-38 Date: 2026-04-23 Status: In Review
Overview
The Ploi provider bridges Shipper's deployment pipeline to the Ploi API. It extends AbstractProvider, registers all 17 operation managers, and uses the ploi/ploi-php-sdk to provision sites, databases, SSL, queues, cron, and more on a Ploi-managed server.
Current Implementation
Key Classes / Files
| File | Class | Responsibility |
|---|---|---|
PloiProvider.php |
PloiProvider |
Entry point; registers all 17 operations; extends AbstractProvider |
PloiClientFactory.php |
PloiClientFactory |
Lazily creates Ploi SDK client; exposes server ID + timeout |
PloiSiteManager.php |
PloiSiteManager |
Find or create site by domain |
PloiRepositoryManager.php |
PloiRepositoryManager |
Install git repository on site |
PloiDatabaseManager.php |
PloiDatabaseManager |
Create databases with interpolated names |
PloiDeploymentExecutor.php |
PloiDeploymentExecutor |
Trigger deployment + poll with log inspection |
PloiDeploymentLogReader.php |
PloiDeploymentLogReader |
Fetch deployment log lines |
PloiSslManager.php |
PloiSslManager |
Provision SSL certificate |
PloiEnvironmentManager.php |
PloiEnvironmentManager |
Write .env file viaMergesEnvironment |
PloiDeployScriptManager.php |
PloiDeployScriptManager |
Set deployment script |
PloiQueueManager.php |
PloiQueueManager |
Configure queue workers |
PloiCronManager.php |
PloiCronManager |
Configure scheduled jobs |
PloiRedirectManager.php |
PloiRedirectManager |
HTTP redirect rules |
PloiDaemonManager.php |
PloiDaemonManager |
Background daemon processes |
PloiNetworkRuleManager.php |
PloiNetworkRuleManager |
Firewall rules |
PloiPhpVersionManager.php |
PloiPhpVersionManager |
PHP version selection |
PloiNginxConfigManager.php |
PloiNginxConfigManager |
Custom Nginx configuration |
PloiAliasManager.php |
PloiAliasManager |
Domain aliases |
PloiServerSiteList.php |
PloiServerSiteList |
List all sites on server |
PloiProvider: Additional Public API
final class PloiProvider extends AbstractProvider {
use InterpolatesNames;
private PloiClientFactory $factory;
public function getName(): string // 'ploi'
public function getServerId(): string
public function getClient(): Ploi // exposes SDK directly
public function getLastSiteId(): int
public function getDeploymentLogs(int $serverId, int $siteId): array<string> // backwards-compat helper
protected function registerOperations(): void // registers all 17 managers
protected function handleException(\Exception $e): string // maps Ploi SDK exceptions
protected function planDatabases(array<DatabaseConfig> $databases, string $projectName, string $profileName): array // InterpolatesNames
}
Validation Rules (in addition to AbstractProvider base validation)
| Field | Rule |
|---|---|
config['api_key'] |
Required, non-empty string |
config['server_id'] |
Required, numeric-only (digits) |
profile['domain'] |
Required, non-empty |
project['repository'] |
Must have provider (github/gitlab/bitbucket/custom) and name (username/repo) |
Exception Mapping
| Exception class | Message prefix |
|---|---|
\Ploi\Exceptions\Http\Unauthenticated |
"Authentication failed: Invalid Ploi API key." |
\Ploi\Exceptions\Http\NotFound |
"Resource not found: Server ID {id} may not exist or you don't have access." |
\Ploi\Exceptions\Http\NotValid |
"Validation error: {message}" |
Other Exception |
"Deployment error: {message} (Type: {class})" (via parent) |
SiteManager apply() Behavior
- Fetch all sites on server via
GET /servers/{id}/sites - Match by
domainproperty - If found: return
SiteResult::found(site->id)— does NOT reinstall repository - If not found: create via
POST /servers/{id}/siteswithweb_directoryandproject_root - If
idis 0 or null in response: returnSiteResult::fail('Invalid response')
DeploymentExecutor apply() Behavior (Ploi)
- Call
site->deployment()->deploy()to trigger deployment - Poll
GET /servers/{id}/sites/{id}every 5 seconds up todeployment_timeout(default 60s) - When
deploying === false:- If
status === 'deploy-failed': fetch logs, returnOperationResult::failwith log excerpt - Otherwise: scan log lines for keywords (
deployment failed,fatal error, etc.) - If any keyword found: return
OperationResult::fail - Else: return
OperationResult::ok()
- If
- If timeout reached: return
OperationResult::fail("Deployment timeout after {n} seconds")
Functional Requirements
FR-001 — Provision site by domain As the CLI, I want to find or create a Ploi site matching the profile's domain so that subsequent operations have a valid site ID.
- Acceptance:
SiteResult::found(id)when domain already exists;SiteResult::created(id)on new site;SiteResult::fail(...)on API error.
FR-002 — Trigger and poll deployment As the CLI, I want to deploy the site via Ploi API and wait for completion so that deployment status is known before the command returns.
- Acceptance: Returns
OperationResult::ok()on success;OperationResult::fail(msg)on deploy-failed status or error keywords in logs; timeout afterdeployment_timeoutseconds.
FR-003 — Read deployment logs As the CLI, I want to fetch deployment log lines from Ploi so that I can surface failures and debug deployment issues.
- Acceptance: Returns
array<int, string>of log lines; empty array if unavailable.
FR-004 — Handle Ploi API exceptions gracefully As the CLI, I want Ploi-specific exceptions to produce user-friendly messages so that debugging is easier.
- Acceptance: Unauthenticated maps to auth error; NotFound maps to server ID error; ValidationException maps to validation error; all others use generic deployment error format.
Edge Cases
- Site create response missing id: Returns
SiteResult::fail('Failed to create site: Invalid response from Ploi API')— does not throw. - Domain already exists: Returns
SiteResult::found()—AbstractProviderskips repository install becauseisNew === false. - Deployment still running at timeout: Returns
OperationResult::fail("Deployment timeout after {n} seconds. Deployment may still be running on Ploi."). - Poll response missing data: Loop continues without incrementing elapsed — safe against null responses.
- Empty site list from API: Proceeds to create site — no false match against existing.
- Delete site not found: Returns
OperationResult::ok()— idempotent destroy.
Acceptance Criteria
- PloiProvider registers all 17 operation managers
-
getName()returns'ploi' -
getServerId()returns the configured numeric server ID - Validation fails cleanly when
api_keyorserver_idis missing/wrong - Site find/create works for both existing and new domains
- Deployment polling respects
deployment_timeoutconfig - Log inspection catches
deployment failed,fatal error,critical errorin logs - SDK exceptions are caught and mapped to user-friendly messages
-
plan()includes all 15+ sub-actions with human-readable labels
Open Questions / Potential Concerns
deployment_timeoutdefault of 60s may be too short for apps with npm asset builds. Should this be configurable per-profile or per-project?getDeploymentLogshelper method onPloiProvideris backwards-compatible but leaksDeploymentLogReaderInterfaceusage. Is this the right API surface, or should logs be retrieved through a command?- Repository re-install on found site: The current design correctly skips this, but there's no option to force a re-clone. Is this intentional?
- No site suspension support: Ploi SDK supports it but Shipper doesn't expose it. Confirm this is out of scope.