PHP
Dagger can be used to perform common CI tasks - testing, containerizing, publishing and more - for any PHP application.
The following code sample demonstrates how these CI tasks can be encapsulated as Dagger Functions in a Dagger module. It assumes:
- A PHP 8.2.x Web application with:
- Composer for package management
- PHPUnit for application testing
- Credentials to publish the containerized application image to a registry
- Go
- Python
- TypeScript
Bootstrap a new module:
dagger init --name=my-module --sdk=go --source=./dagger
Update the generated dagger/main.go
file with the following code:
package main
import (
"context"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// return container image with application source code and dependencies
func (m *MyModule) Build(source *dagger.Directory) *dagger.Container {
return dag.Container().
From("php:8.2-apache-buster").
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "--yes", "git-core", "zip", "curl"}).
WithExec([]string{"docker-php-ext-install", "pdo", "pdo_mysql", "mysqli"}).
WithExec([]string{"sh", "-c", "sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf"}).
WithExec([]string{"sh", "-c", "sed -ri -e 's!/var/www/!/var/www/public!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf"}).
WithExec([]string{"a2enmod", "rewrite"}).
WithExec([]string{"sh", "-c", "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer"}).
WithDirectory("/var/www", source.WithoutDirectory("dagger"), dagger.ContainerWithDirectoryOpts{
Owner: "www-data",
}).
WithWorkdir("/var/www").
WithExec([]string{"chmod", "-R", "775", "/var/www"}).
WithMountedCache("/root/.composer", dag.CacheVolume("composer-cache")).
WithMountedCache("/var/www/vendor", dag.CacheVolume("composer-vendor-cache")).
WithExec([]string{"composer", "install"})
}
// return result of unit tests
func (m *MyModule) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return m.Build(source).
WithEnvVariable("PATH", "./vendor/bin:$PATH", dagger.ContainerWithEnvVariableOpts{
Expand: true,
}).
WithExec([]string{"phpunit"}).
Stdout(ctx)
}
// return address of published container image
func (m *MyModule) Publish(ctx context.Context, source *dagger.Directory, version string, registryAddress string, registryUsername string, registryPassword *dagger.Secret, imageName string) (string, error) {
return m.Build(source).
WithLabel("org.opencontainers.image.title", "PHP with Dagger").
WithLabel("org.opencontainers.image.version", version).
// uncomment this to use a custom entrypoint file
// .WithExec([]string{"chmod", "+x", "/var/www/docker-entrypoint.sh"}).
// .WithEntrypoint([]string{"/var/www/docker-entrypoint.sh"}).
WithRegistryAuth(registryAddress, registryUsername, registryPassword).
Publish(ctx, fmt.Sprintf("%s/%s/%s", registryAddress, registryUsername, imageName))
}
Bootstrap a new module:
dagger init --name=my-module --sdk=python --source=./dagger
Update the generated dagger/src/main/__init__.py
file with the following code:
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""Return container image with application source code and dependencies"""
return (
dag.container()
.from_("php:8.2-apache-buster")
.with_exec(["apt-get", "update"])
.with_exec(["apt-get", "install", "--yes", "git-core", "zip", "curl"])
.with_exec(["docker-php-ext-install", "pdo", "pdo_mysql", "mysqli"])
.with_exec(
[
"sh",
"-c",
(
"sed -ri -e 's!/var/www/html!/var/www/public!g'"
"/etc/apache2/sites-available/*.conf"
),
]
)
.with_exec(
[
"sh",
"-c",
(
"sed -ri -e 's!/var/www/!/var/www/public!g'"
" /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf"
),
]
)
.with_exec(["a2enmod", "rewrite"])
.with_exec(
[
"sh",
"-c",
(
"curl -sS https://getcomposer.org/installer"
" | php -- --install-dir=/usr/local/bin --filename=composer"
),
]
)
.with_directory(
"/var/www",
source.without_directory("dagger"),
owner="www-data",
)
.with_workdir("/var/www")
.with_exec(["chmod", "-R", "775", "/var/www"])
.with_mounted_cache("/root/.composer", dag.cache_volume("composer-cache"))
.with_mounted_cache(
"/var/www/vendor",
dag.cache_volume("composer-vendor-cache"),
)
.with_exec(["composer", "install"])
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Return result of unit tests"""
return (
await self.build(source)
.with_env_variable("PATH", "./vendor/bin:$PATH", expand=True)
.with_exec(["phpunit"])
.stdout()
)
@function
async def publish(
self,
source: dagger.Directory,
version: str,
registry_address: str,
registry_username: str,
registry_password: dagger.Secret,
image_name: str,
) -> str:
"""Return address of published container image"""
return await (
self.build(source)
.with_label("org.opencontainers.image.title", "PHP with Dagger")
.with_label("org.opencontainers.image.version", version)
# uncomment this to use a custom entrypoint file
# .with_exec(["chmod", "+x", "/var/www/docker-entrypoint.sh"])
# .with_entrypoint(["/var/www/docker-entrypoint.sh"])
.with_registry_auth(registry_address, registry_username, registry_password)
.publish(f"{registry_address}/{registry_username}/{image_name}")
)
Bootstrap a new module:
dagger init --name=my-module --sdk=typescript --source=./dagger
Update the generated dagger/src/index.ts
file with the following code:
import {
dag,
Container,
Directory,
Secret,
object,
func,
} from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Return container image with application source code and dependencies
*/
@func()
build(source: Directory): Container {
return dag
.container()
.from("php:8.2-apache-buster")
.withExec(["apt-get", "update"])
.withExec(["apt-get", "install", "--yes", "git-core", "zip", "curl"])
.withExec(["docker-php-ext-install", "pdo", "pdo_mysql", "mysqli"])
.withExec([
"sh",
"-c",
"sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf",
])
.withExec([
"sh",
"-c",
"sed -ri -e 's!/var/www/!/var/www/public!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf",
])
.withExec(["a2enmod", "rewrite"])
.withDirectory("/var/www", source.withoutDirectory("dagger"), {
owner: "www-data",
})
.withWorkdir("/var/www")
.withExec(["chmod", "-R", "775", "/var/www"])
.withExec([
"sh",
"-c",
"curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer",
])
.withMountedCache(
"/root/.composer/cache",
dag.cacheVolume("composer-cache"),
)
.withMountedCache(
"/var/www/vendor",
dag.cacheVolume("composer-vendor-cache"),
)
.withExec(["composer", "install"])
}
/*
* Return result of unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await this.build(source)
.withEnvVariable("PATH", "./vendor/bin:$PATH", { expand: true })
.withExec(["phpunit"])
.stdout()
}
/*
* Return address of published container image
*/
@func()
async publish(
source: Directory,
version: string,
registryAddress: string,
registryUsername: string,
registryPassword: Secret,
imageName: string,
): Promise<string> {
const image = this.build(source)
.withLabel("org.opencontainers.image.title", "Laravel with Dagger")
.withLabel("org.opencontainers.image.version", version)
// uncomment this to use a custom entrypoint file
// .withExec(["chmod", "+x", "/var/www/docker-entrypoint.sh"])
// .withEntrypoint(["/var/www/docker-entrypoint.sh"])
const address = await image
.withRegistryAuth(registryAddress, registryUsername, registryPassword)
.publish(`${registryAddress}/${registryUsername}/${imageName}`)
return address
}
}
The code sample above is illustrative only. Modify it to your application's specific requirements.
Here is an example of calling the Dagger Function to run an application's unit tests:
dagger call test --source=.
Here is an example of calling the Dagger Function to publish the application image to Docker Hub. Replace the DOCKER-HUB-USERNAME
and DOCKER-HUB-PASSWORD
placeholders with your Docker Hub credentials.
export REGISTRY_PASSWORD=DOCKER-HUB-PASSWORD
dagger call publish \
--source=. \
--version=0.1 \
--registry-address=docker.io \
--registry-username=DOCKER-HUB-USERNAME \
--registry-password=env:REGISTRY_PASSWORD \
--image-name=my-php-app
Some PHP applications may need to perform specific operations on startup, such as running database migrations or reading/writing cache data. This can be accomplished by overriding the container's default entrypoint with a custom entrypoint script for startup operations.
To use such a script, uncomment the relevant lines in the code sample above and create a script named docker-entrypoint.sh
in the application directory. Here is an example of one such script, which runs database migrations for a Laravel Web application at startup:
#!/bin/bash
php artisan migrate
apache2-foreground