refactor: move into a website directory

This commit is contained in:
Oliver Davies 2022-07-13 18:09:09 +01:00
parent 86529d7148
commit 3c5c0e808a
747 changed files with 133 additions and 2 deletions

1
website/.dockerignore Normal file
View file

@ -0,0 +1 @@
/workspace.yml

29
website/.editorconfig Normal file
View file

@ -0,0 +1,29 @@
# This file is used by editors and IDEs to unify coding standards
# @see http://EditorConfig.org
# @standards PHP: http://www.php-fig.org/psr/psr-2/
root = true
# Default configuration (applies to all file types)
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
indent_style = space
# Markdown customizations
[*.md]
trim_trailing_whitespace = false
[docker-compose.yaml,Dockerfile]
indent_size = 2
[*.pcss]
indent_size = 2
[{postcss,tailwind,webpack}.config.js]
indent_size = 2
[tools/tailwindcss/**/*.js]
indent_size = 2

31
website/.env.example Normal file
View file

@ -0,0 +1,31 @@
COMPOSE_PROJECT_NAME=oliverdavies-uk
# The volume to store the generated output files.
#
# For development, an anonymous volume should be sufficient as the generated
# files don't need to be accessible locally.
#
# For production, you may want to output the files into a directory so that
# they can be served by a web server like Apache or Nginx.
#DOCKER_OUTPUT_VOLUME=./output_dev:/app/output_dev
#DOCKER_OUTPUT_VOLUME=./output_prod:/app/output_prod
DOCKER_OUTPUT_VOLUME=/app/output_dev
# The environment to generate the site for (e.g. dev or prod).
# SCULPIN_ENV=prod
# NODE_ENV=production
SCULPIN_ENV=dev
NODE_ENV=development
# The port on which to serve the site if the --server option is specified.
#SCULPIN_PORT=80
SCULPIN_PORT=8000
# The default URL to use if one hasn't been specified in the environment's
# configuration file.
#SCULPIN_URL=https://example.com
SCULPIN_URL=http://localhost
# Any additional arguments to pass to the "sculpin generate" command.
#SCULPIN_GENERATE_ARGS=
SCULPIN_GENERATE_ARGS="--output-dir=/output/html --server --watch"

8
website/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
/.env
/.phpunit.cache
/build/
/docker-compose.override.yaml
/output/
/output_*/
/source/build/
/vendor/

1
website/.yarnrc Normal file
View file

@ -0,0 +1 @@
--modules-folder /node_modules

74
website/Dockerfile Normal file
View file

@ -0,0 +1,74 @@
FROM node:14-alpine AS assets
ARG NODE_ENV="production"
ARG SCULPIN_ENV="prod"
ENV NODE_ENV="${NODE_ENV}" \
PATH="${PATH}:/node_modules/.bin" \
SCULPIN_ENV="${SCULPIN_ENV}"
RUN apk add --no-cache bash
WORKDIR /app
RUN mkdir -p /node_modules \
&& chown node:node -R /app /node_modules
USER node
COPY --chown=node:node *yarn* package.json ./
RUN yarn install && yarn cache clean
COPY --chown=node:node . .
RUN if [ "${NODE_ENV}" != "development" ]; then \
./run yarn:build:css && ./run yarn:build:js; \
else mkdir -p /app/build; fi
CMD ["bash"]
###
FROM opdavies/sculpin-serve AS app
WORKDIR /app
RUN mkdir /output \
&& chown sculpin:sculpin -R /output
###
FROM app AS build
ENV PATH=$PATH:/app/vendor/bin/phpunit
COPY tools/docker/images/app/root /
WORKDIR /app
USER sculpin
COPY --chown=sculpin:sculpin composer.* ./
RUN composer install --no-dev
COPY --chown=sculpin:sculpin app app
COPY --chown=sculpin:sculpin source source
COPY --chown=sculpin:sculpin src src
RUN sculpin generate --env prod --output-dir /output/html
COPY --chown=sculpin:sculpin . .
COPY --chown=sculpin:sculpin --from=assets /app/build /build
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
CMD ["bash"]
###
FROM alpine AS production
COPY --from=build /output/html /app
CMD ["sh"]

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Opdavies\Sculpin\Bundle\GistEmbedBundle\SculpinGistEmbedBundle;
use Opdavies\Sculpin\Bundle\TwigMarkdownBundle\SculpinTwigMarkdownBundle;
use Sculpin\Bundle\SculpinBundle\HttpKernel\AbstractKernel;
final class SculpinKernel extends AbstractKernel
{
/**
* {@inheritdoc}
*/
protected function getAdditionalSculpinBundles(): array
{
return [
SculpinGistEmbedBundle::class,
SculpinTwigMarkdownBundle::class,
];
}
}

View file

@ -0,0 +1,16 @@
sculpin_content_types:
pages:
permalink: /:basename/
posts:
permalink: blog/:basename/
taxonomies: [tags]
projects:
layout: default
permalink: projects/:basename/
talks:
permalink: talks/:basename/
services:
App\TwigExtension\TalkExtension:
tags:
- { name: twig.extension }

View file

@ -0,0 +1,75 @@
name: Oliver Davies
description: Drupal Developer and Consultant
locale: en-GB
avatar:
path: "/images/social-avatar.jpg"
drupalorg:
name: opdavies
url: 'https://www.drupal.org/u/%drupalorg.name%'
email: oliver@oliverdavies.uk
experience:
start_year: 2007
github:
gist:
url: 'https://gist.github.com/%github.name%'
name: opdavies
url: 'https://github.com/%github.name%'
linkedin:
name: opdavies
url: 'https://www.linkedin.com/in/%linkedin.name%'
menus:
footer:
- title: About
href: /
is_active: '^//$'
- title: Blog
href: /blog
is_active: '^/blog/?'
- title: Talks
href: /talks
is_active: '^/talks/?'
- title: Recommendations
href: /recommendations
- title: RSS feed
href: /rss.xml
main: []
packagist:
name: opdavies
url: 'https://packagist.org/packages/%packagist.name%'
plausible:
domain: ~
savvycal:
name: opdavies
url: 'https://savvycal.com/%savvycal.name%'
speakerdeck:
name: opdavies
url: 'https://speakerdeck.com/%speakerdeck.name%'
twitter:
name: opdavies
url: 'https://twitter.com/%twitter.name%'
work:
company:
name: Transport for Wales
url: https://tfw.wales
role: Lead Software Developer
youtube:
channel:
id: UCkeK0qF9HHUPQH_fvn4ghqQ
url: 'https://www.youtube.com/channel/%youtube.channel.id%'

View file

@ -0,0 +1,8 @@
---
imports:
- sculpin_site.yml
plausible:
domain: 'oliverdavies.uk'
url: https://www.oliverdavies.uk

View file

@ -0,0 +1,26 @@
@layer base {
h2 {
@apply font-bold
}
blockquote {
@apply pl-4 border-l-3 border-blue-primary dark:border-blue-400;
}
code {
@apply px-2 py-1 text-sm rounded-md text-gray-700 bg-gray-200 dark:bg-gray-800 dark:text-gray-100;
}
pre {
@apply p-4 my-8 overflow-auto rounded-md text-gray-700 bg-gray-200 dark:bg-gray-800 dark:text-gray-100;
}
iframe {
@apply w-full;
}
a:focus {
@apply outline-black dark:outline-white;
outline-offset: 2px !important;
}
}

View file

@ -0,0 +1,66 @@
@layer components {
.link {
@apply underline text-blue-primary hover:text-blue-900 dark:text-blue-400 dark:hover:text-white;
text-decoration-thickness: 1px;
text-underline-offset: 0.1em;
}
.markdown {
> * + * {
@apply mt-4;
}
> *:first-child {
@apply mt-0;
}
h2 + * {
@apply mt-2;
}
h2 {
@apply mt-6;
}
h3 {
@apply mt-8 font-bold;
}
h2 + h3 {
@apply mt-2
}
blockquote {
@apply my-8;
}
ul {
@apply pl-6 list-disc;
}
li {
@apply mt-1 first:mt-0;
}
a {
@apply link;
}
pre {
@apply my-8;
}
.media--type-image {
@apply my-8;
}
.speakerdeck-embed-wrapper,
.video-full {
@apply my-8 aspect-w-4 aspect-h-3;
}
}
.visually-hidden {
@apply sr-only;
}
}

View file

@ -0,0 +1,7 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import './base.pcss';
@import './components.pcss';
@import './utilities.pcss';

View file

@ -0,0 +1,15 @@
@layer utilities {
@variants dark {
.text-decoration-blue-400 {
text-decoration-color: theme('colors.blue.400');
}
.text-decoration-blue-primary {
text-decoration-color: theme('colors.blue.800');
}
.text-decoration-white {
text-decoration-color: theme('colors.white');
}
}
}

20
website/assets/js/app.js Normal file
View file

@ -0,0 +1,20 @@
import 'alpinejs'
import 'focus-visible'
import bash from 'highlight.js/lib/languages/bash'
import hljs from 'highlight.js/lib/core'
import ini from 'highlight.js/lib/languages/ini'
import javascript from 'highlight.js/lib/languages/javascript'
import php from 'highlight.js/lib/languages/php'
import yaml from 'highlight.js/lib/languages/yaml'
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('ini', ini);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('php', php);
hljs.registerLanguage('yaml', yaml);
document.addEventListener('DOMContentLoaded', event => {
document.querySelectorAll('pre code').forEach(block => {
hljs.highlightBlock(block)
})
})

50
website/composer.json Normal file
View file

@ -0,0 +1,50 @@
{
"name": "opdavies/sculpin-skeleton",
"description": "A skeleton Sculpin site.",
"license": "MIT",
"authors": [
{
"name": "Oliver Davies",
"email": "oliver@oliverdavies.uk",
"homepage": "https://www.oliverdavies.uk"
}
],
"require": {
"illuminate/collections": "^8.55",
"nesbot/carbon": "^2.52",
"opdavies/sculpin-gist-embed-bundle": "^0.1.0",
"opdavies/sculpin-twig-markdown-bundle": "^0.2.0",
"sculpin/sculpin": "^3.0"
},
"scripts": {
"dev": "composer run-script generate",
"generate": "sculpin generate --clean --no-interaction",
"prod": "composer run-script generate -- --env prod",
"watch": "composer run-script --timeout=0 generate -- --server --watch"
},
"config": {
"sort-packages": true
},
"require-dev": {
"phpstan/phpstan": "^0.12.98",
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"extra": {
"violinist": {
"one_pull_request_per_package": 1,
"run_scripts": 0,
"bundled_packages": {},
"blacklist": []
}
}
}

6293
website/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
x-assets: &default-assets
build:
context: "."
target: "assets"
env_file:
- ".env"
restart: "${DOCKER_RESTART_POLICY:-unless-stopped}"
stop_grace_period: "3s"
tty: true
volumes:
- ".:/app"
services:
css:
<<: *default-assets
command: "./run yarn:build:css"
js:
<<: *default-assets
command: "./run yarn:build:js"

View file

@ -0,0 +1,22 @@
services:
app:
build:
context: .
target: build
image: "ghcr.io/opdavies/oliverdavies.uk-web:${DOCKER_TAG:-latest}"
command: "sculpin generate --clean --no-interaction --url ${SCULPIN_URL:-http://localhost} --env ${SCULPIN_ENV:-dev} ${SCULPIN_GENERATE_ARGS}"
volumes:
- .:/app
tty: true
expose:
- 8000
networks:
- web
labels:
- "traefik.docker.network=traefik_proxy"
- "traefik.http.routers.oliverdavies.rule=Host(`oliverdavies.docker.localhost`)"
networks:
web:
external:
name: traefik_proxy

21
website/drafts Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
set -o pipefail
readonly DRAFTS_DIR="${HOME}/Code/Personal/oliverdavies.uk/source/_posts"
if [[ $# -eq 0 ]]; then
grep -lr "draft: true" "${DRAFTS_DIR}" | cut -d"/" -f9- | tr ".md" "" | sort
exit 0
fi
readonly DRAFTS_FILE="${*}.md"
readonly DRAFTS_PATH="${DRAFTS_DIR}/${DRAFTS_FILE// /-}"
echo $DRAFTS_PATH
if [[ -e "${DRAFTS_PATH}" ]]; then
eval "${EDITOR}" "${DRAFTS_PATH}"
else
touch "${DRAFTS_PATH}"
fi

28
website/esbuild.config.js Normal file
View file

@ -0,0 +1,28 @@
const esbuild = require('esbuild')
let minify = false
let sourcemap = true
let watch_fs = true
if (process.env.NODE_ENV === 'production') {
minify = true
sourcemap = false
watch_fs = false
}
const watch = watch_fs && {
onRebuild(error) {
if (error) console.error('[watch] build failed', error)
else console.log('[watch] build finished')
},
}
esbuild.build({
entryPoints: ['./assets/js/app.js'],
outfile: './build/app.js',
bundle: true,
minify: minify,
sourcemap: sourcemap,
watch: watch,
plugins: [],
})

22
website/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"build:css": "./run yarn:build:css"
},
"dependencies": {
"@tailwindcss/aspect-ratio": "^0.2.0",
"@tailwindcss/forms": "^0.2.1",
"@tailwindcss/typography": "^0.4.0",
"alpinejs": "^2.3.5",
"autoprefixer": "^10.2.5",
"elliptic": ">=6.5.3",
"esbuild": "^0.14.10",
"focus-visible": "^5.1.0",
"highlight.js": "^10.4.1",
"lodash": ">=4.17.19",
"postcss": "^8.2.1",
"postcss-easy-import": "^3.0.0",
"postcss-nested": "^5.0.5",
"tailwindcss": "2"
}
}

4
website/phpstan.neon Normal file
View file

@ -0,0 +1,4 @@
parameters:
level: max
paths:
- src

26
website/phpunit.xml.dist Normal file
View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
colors="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

10
website/postcss.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
plugins: {
'postcss-easy-import': {
extensions: ['.css', '.pcss']
},
tailwindcss: {},
'postcss-nested': {},
autoprefixer: {}
}
}

103
website/run Executable file
View file

@ -0,0 +1,103 @@
#!/bin/bash
set -eux
TTY=""
if [[ ! -t 1 ]]; then
TTY="-T"
fi
DC="${DC:-exec}"
function ci:build-images {
docker build --target production -t ghcr.io/opdavies/oliverdavies.uk-build:latest .
docker build --target production -t ghcr.io/opdavies/oliverdavies.uk-build:$(git rev-parse HEAD) .
}
function ci:push-images {
docker push ghcr.io/opdavies/oliverdavies.uk-build:latest
docker push ghcr.io/opdavies/oliverdavies.uk-build:$(git rev-parse HEAD)
}
function cmd {
# Run any command in the app container.
_dc app "${@}"
}
function composer {
DC=run
_dc --entrypoint composer app "${@}"
}
function deploy {
cd tools/deployment
ansible-playbook deploy.yml "${@}"
}
function help {
printf "%s <task> [args]\n\nTasks:\n" "${0}"
compgen -A function | grep -v "^_" | cat -n
printf "\nExtended help:\n Each task has comments for general usage\n"
}
function run-production {
DESTINATION_PATH="${2:-/var/www/oliverdavies.uk}"
GIT_COMMIT_HASH="${1:-latest}"
# Clean up any old containers or files within the artifact directory.
rm -fr ${DESTINATION_PATH}/* || true
docker image pull ghcr.io/opdavies/oliverdavies-uk-web:${GIT_COMMIT_HASH}
docker container run \
--entrypoint sh \
--name oliverdavies-uk-web \
ghcr.io/opdavies/oliverdavies-uk-web:${GIT_COMMIT_HASH}
docker container cp oliverdavies-uk-web:/code/. ${DESTINATION_PATH}
docker container rm oliverdavies-uk-web
}
function sh {
DC=run
_dc --entrypoint sh app
}
function test:quality {
DC=run
_dc --entrypoint phpstan app "${@}" analyze --memory-limit=-1
}
function test:unit {
DC=run
_dc --entrypoint phpunit app "${@}"
}
function yarn:build:css {
# Build CSS assets, this is meant to be run from within the assets container.
local args=()
if [ "${SCULPIN_ENV}" == "prod" ]; then
args=(--minify)
elif [ "${NODE_ENV}" == "development" ]; then
args=(--watch)
fi
tailwindcss --postcss \
-i assets/css/tailwind.pcss \
-o source/build/app.css "${args[@]}"
}
function yarn:build:js {
# Build JS assets, this is meant to be run from within the assets container.
node esbuild.config.js
}
function _dc {
docker-compose ${DC} ${TTY} "${@}"
}
eval "${@:-help}"

View file

@ -0,0 +1,16 @@
<aside>
<h2>About Me</h2>
<div class="flex mt-4 space-x-4">
<div class="flex-shrink-0">
<img src="{{ avatar.path }}" alt="Picture of Oliver" class="w-16 h-16 rounded-full border border-gray">
</div>
<div>
<p>
Oliver Davies is a PHP Developer and Linux Systems Administrator based in the UK.
He is a Full Stack Software Consultant specialising in Drupal application development, and a {{ work.role }} at <a href="{{ work.company.url }}?utm_source=oliverdavies.uk&amp;utm_medium=about-author" class="link">{{ work.company.name }}</a>.
</p>
</div>
</div>
</aside>

View file

@ -0,0 +1,8 @@
<figure class="block">
<img src="{{ image.src }}" alt="{{ image.alt }}" class="p-1 border">
{% if caption %}
<figcaption class="mt-1 mb-0 text-sm italic text-center text-gray-800">
{{ caption }}
</figcaption>
{% endif %}
</figure>

View file

@ -0,0 +1,7 @@
<footer>
<nav class="flex flex-wrap justify-center -mb-3">
{% for link in site.menus.footer %}
<a class="mx-3 mb-3 text-sm md:text-lg dark:text-white hover:text-gray-900 link dark:hover:text-blue-400" href="{{ link.href }}">{{ link.title }}</a>
{% endfor %}
</nav>
</footer>

View file

@ -0,0 +1 @@
<hr class="my-12 border-t border-gray-300 dark:border-gray-500"/>

View file

@ -0,0 +1,12 @@
<a
class="
inline-flex items-center px-6 py-3 font-medium rounded-md bg-blue-primary text-white no-underline hover:bg-white hover:text-blue-primary focus:bg-white focus:text-blue-primary transition-color ease-in-out duration-200
{{ size == 'normal' ? 'text-base' }}
{{ size == 'large' ? 'text-lg' }}
"
href="{{ href }}"
>
{% block text '' %}
{% if arrow %}{% endif %}
</a>

View file

@ -0,0 +1,48 @@
{% set currentUrl = site.url ~ page.url|trim('/', 'right') %}
{% set pageTitle = page.title %}
{% set siteTitle = site.name %}
{% if not page.is_front %}
{% set pageTitle = [page.title, '-', site.name]|join(' ') %}
{% endif %}
{% if page.meta.title %}
{% set pageTitle = page.meta.title %}
{% endif %}
{% set metaDescription = site.description %}
{% if page.meta.description %}
{% set metaDescription = page.meta.description|e('html') %}
{% elseif page.description %}
{% set metaDescription = page.description|e('html') %}
{% elseif page.excerpt %}
{% set metaDescription = page.excerpt|e('html') %}
{% endif %}
{% set metaImage = [
site.url,
'/',
page.meta.image ?? site.avatar.path,
]|join %}
<title>{{ pageTitle }}</title>
<link rel="canonical" href="{{ currentUrl }}" />
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:site_name" content="{{ siteTitle }}" />
<meta property="og:title" content="{{ pageTitle }}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ currentUrl }}" />
<meta name="description" content="{{ metaDescription }}">
<meta name="og:description" content="{{ metaDescription }}">
<meta name="twitter:description" content="{{ metaDescription }}">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="{{ metaImage }}" />
<meta name="twitter:image:alt" content="Page image for {{ site.name }}" />
<meta property="og:image" content="{{ metaImage }}" />
<meta property="og:image:alt" content="Page image for {{ site.name }}" />

View file

@ -0,0 +1,30 @@
<div>
<div class="py-4 px-4 mx-auto max-w-2xl">
<div class="flex flex-col justify-between items-center md:flex-row">
<div>
<a href="/">
<svg
aria-hidden="true"
class="w-16 h-16 fill-current dark:text-blue-400 text-blue-primary md:w-18 md:h-18"
viewBox="0 0 706 504"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M456.5 1.1c-12.3 1.5-31 5.5-44.1 9.4-12.7 3.9-63.6 24.6-64.1 26.2-.2.5 1.4 1.7 3.4 2.7 2.1 1 8.9 5.1 15.1 9.2l11.2 7.5 14.5-6c22.9-9.5 37.3-14 57.5-17.8 7.2-1.3 14.7-1.7 31-1.8 18.6 0 23.1.3 33 2.3 22 4.5 46.1 13.9 64.5 25.2 39.3 24.2 69.9 65.3 86.9 116.5 16.3 49.2 13 100.2-9.4 145.3-21.8 43.7-49.2 68.8-101.5 92.9-13.4 6.2-120.1 51.3-121.3 51.3-.5 0-15.7-35.1-33.7-78l-32.8-78 3.1-12.8c4.4-18 5.6-29.5 4.9-48.5-.6-16.8-2-25.7-6.3-38.7-12-35.8-40.8-69.2-74-85.6l-7.2-3.6-4.2-10c-2.4-5.6-3.9-10.3-3.4-10.7.5-.5 9.7-4.5 20.4-9s19.9-8.6 20.4-9.1c1.5-1.5-18.6-10.1-32.3-13.9l-6.8-1.9-20.6 8.7c-11.4 4.8-20.9 8.9-21.2 9.2-.2.3 2 6.3 5 13.3 3 7 5.5 12.8 5.5 12.9 0 .1-7.5.2-16.7.2-11.8.1-19.4.6-25.6 1.8-27.3 5.5-50.5 17.6-70.4 37-21.8 21.2-36.7 49-43 80.2-2.4 12.3-2.4 44 0 57.2 3.6 19.4 11.9 40.4 22.3 56 6.9 10.4 21.1 25.4 31.2 33 29.9 22.5 70.8 33.2 106.2 27.8 18.9-2.8 39.3-10.6 54.1-20.5 13.5-9.1 29.1-23.8 37.6-35.5 1.2-1.7 2.4-2.8 2.7-2.5.3.3 15.6 36.6 34.1 80.5 18.5 44 33.8 80.1 33.9 80.3.8.8 144.9-60.8 162.1-69.3 45.5-22.4 73.4-47.1 95.7-84.7 28-47.4 37.5-99.7 27.8-153.5-6.8-37.6-25-79-48.6-110.3-33.2-44.1-83-74.2-138.4-83.6-11.4-1.9-46.9-2.7-58.5-1.3zM259.2 141.4c42.4 10.9 77.8 50 84.8 93.8 1.6 9.9.8 34.5-1.4 44.8-5.2 24-15.5 43-32.6 60-20.7 20.6-42.8 31.3-67.7 32.7-26.9 1.5-53.2-6.2-74.3-21.7-29.4-21.7-46-56.2-46-95.7 0-45.4 27.2-89.6 66.1-107.2 8.2-3.7 21.7-7.9 29.4-9.1 10.4-1.6 30.8-.4 41.7 2.4z"/><path d="M201 20.6c-83 11.2-157 71-186.5 150.8-22.3 60.3-18.3 134.9 10.2 192 21.5 43.1 59.6 81.6 102.1 103.4 21.1 10.9 46.3 19 71.2 22.9 16.2 2.5 53.1 2.5 68.5 0 25.9-4.2 45.2-10.5 69-22.2 14.4-7.1 39.7-23.2 41.8-26.7.8-1.2-.2-4.6-4.3-14.5-2.9-7-5.7-13.2-6.1-13.7-.5-.5-4.3 1.7-8.6 5.1C320 447.1 277.6 462 232 462c-59.8 0-115.2-26.3-154.8-73.5-32.2-38.3-48.8-88.7-46.9-142 2-53.7 22.1-99.6 60.7-138.5 28.1-28.3 63-47.2 102.9-55.7 11.9-2.6 14.1-2.8 38.6-2.8 28.3 0 39.4 1.3 59 7 27.9 8.1 58.5 26.1 80.9 47.6l10.9 10.5-14.3 6c-7.8 3.2-14.5 6.4-14.7 7-.2.7 16.5 41.3 37.1 90.4 20.7 49.1 37.6 90.1 37.6 91.1 0 3.4-7.1 24.2-11.6 33.8-2.4 5.1-6.6 13.1-9.4 17.7l-5 8.3 6.6 15.6c5.5 12.9 6.9 15.4 8 14.4 2.8-2.3 19.2-27.8 24.4-37.9l5.3-10.3 8.3 19.8c4.7 10.9 8.7 20.1 8.9 20.3.6.6 60.8-24.6 74.5-31.2 29.7-14.2 52.7-35.8 65.3-61.1 16.9-34 17.6-70.8 2.2-112-17-45.3-45.8-76.7-82.5-90-18.2-6.5-43.1-9.1-63.5-6.5-11.5 1.4-30 5.8-40.3 9.5l-7.3 2.6L402.3 91c-20.3-21.3-37.9-34.3-65.4-48.3-33.4-17.1-63.7-23.8-105.9-23.6-10.7.1-24.2.7-30 1.5zM504.5 122c9.2 2.5 22.1 8.3 29.2 13.1 6.9 4.7 18.7 16.3 24.3 23.9 15.1 20.6 26.3 49 29 74 2.3 20.7-3.1 43-14.5 60.5-8.7 13.3-27.6 29.5-44.5 38-6 3.1-47.3 20.6-47.5 20.2-1.1-1.6-87.5-208-87.3-208.3.2-.2 7.4-3.3 15.8-6.9 22.5-9.5 33.8-13 55-16.9 1.4-.2 9.3-.3 17.5-.1 11.5.2 16.9.8 23 2.5z"/>
</svg>
<span class="sr-only">
{{ site.name }}
</span>
</a>
</div>
<div>
<nav class="flex items-center mt-4 space-x-6 md:flex-row md:mt-0">
{% for item in site.menus.main %}
{% set is_active = page.url matches '#' ~ item.is_active ~ '#' %}
<a class="text-black dark:text-white border-b-3 py-2 hover:border-gray-300 {{ is_active ? 'border-blue-primary dark:border-blue-400' : 'border-transparent' }}" href="{{ item.href }}">{{ item.title }}</a>
{% endfor %}
</nav>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,4 @@
{% if plausible.domain %}
<script defer data-domain="{{ plausible.domain }}" src="https://plausible.io/js/plausible.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
{% endif %}

View file

@ -0,0 +1,3 @@
<footer>
<p>Comments or questions? I'm <a class="link" href="{{ twitter.url }}">@{{ twitter.name }}</a> on Twitter.</p>
</footer>

View file

@ -0,0 +1,15 @@
{% macro shouldDisplayOldPostMessage(post) %}
{% set cutOffDate = 'today -1 year'|date('U') %}
{% if post.date is not empty %}
{{ post.date <= cutOffDate }}
{% endif %}
{% endmacro %}
{% import _self as helpers %}
{% if helpers.shouldDisplayOldPostMessage(post)|trim %}
<div class="p-6 my-10 border border-gray-300 dark:bg-gray-800 dark:border-gray-700">
<p><strong>Warning:</strong> This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.</p>
</div>
{% endif %}

View file

@ -0,0 +1,13 @@
<article>
<h2>
<a class="dark:text-blue-400 text-blue-primary" href="{{ post.url|trim('/', 'right') }}">
{{ post.title }}
</a>
</h2>
<time class="text-base" datetime="{{ post.date|date('Y-m-d') }}">
{{ post.date|date('jS F Y') }}
</time>
<p class="mt-1">{{ post.excerpt }}</p>
</article>

View file

@ -0,0 +1,9 @@
<div class="slides">
<noscript>**Please enable JavaScript to view slides.**</noscript>
<script
class="speakerdeck-embed"
data-id="{{ data.id }}"
data-ratio="{{ data.ratio ?: '1.29456384323641' }}"
src="//speakerdeck.com/assets/embed.js"
></script>
</div>

View file

@ -0,0 +1,10 @@
<li>
{% if url %}
<a href="{{ url }}">{{ name }}</a>
{% else %}
{{ name }}
{% endif %}
{% if location %}in {{ location }}{% endif %}
- {{ date|date('jS F Y') }}
{{ is_online ? '(online)' }}
</li>

View file

@ -0,0 +1,17 @@
{% if events is not empty %}
<div class="markdown">
<h2>Events</h2>
<ul>
{% for event in events|sort((a, b) => a.date <=> b.date) %}
{% include 'talk/event-list-event.html.twig' with {
date: event.date,
is_online: event.is_online ?? false,
location: event.location,
name: event.name,
url: event.url,
} only %}
{% endfor %}
</ul>
</div>
{% endif %}

View file

@ -0,0 +1,9 @@
{% if speakerdeck.id and speakerdeck.ratio %}
<div>
<h2 class="mb-2">Slides</h2>
{% include 'speakerdeck' with {
data: speakerdeck,
} only %}
</div>
{% endif %}

View file

@ -0,0 +1,28 @@
{% macro videoSrc(video) %}
{% set srcUrls = {
youtube: '//www.youtube.com/embed',
videopress: 'https://videopress.com/embed',
vimeo: 'https://player.vimeo.com/video',
} %}
{{ srcUrls[video.type] ~ '/' ~ video.id }}
{% endmacro %}
{% from _self import videoSrc %}
{% if video.id %}
<div class="mt-4">
<h2 class="mb-2">Video</h2>
<div class="video-full">
<iframe
width="678"
height="408"
src="{{ videoSrc(video) }}"
frameborder="0"
allowfullscreen
>
</iframe>
</div>
</div>
{% endif %}

View file

@ -0,0 +1,17 @@
<p><strong>Enter your email address to subscribe to the Test-Driven Drupal mailing list
and be notified of any updates.</strong></p>
<div class="w-full lg:w-2/3">
<form action="https://oliverdavi.us18.list-manage.com/subscribe/post?u=b4ac8dd177796d37b93f9c285&amp;amp;id=033c84e0d5" method="post" name="mc-embedded-subscribe-form" novalidate="">
<div id="mc_embed_signup_scroll">
<div style="position: absolute; left: -5000px;" aria-hidden="true">
<input type="text" name="b_46d1ff41a9918b3b7efb885dc_6df88a3d0f" tabindex="-1" value="">
</div>
<div class="flex overflow-hidden">
<input type="email" value="" name="EMAIL" class="block p-3 pl-5 w-full rounded-l-full border required email border-gray" placeholder="enter your email" aria-label="Email">
<button type="submit" name="subscribe" class="block py-3 pr-6 pl-5 w-auto text-white bg-blue-600 rounded-r-full border border-l-0 border-gray">Subscribe</button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,10 @@
<div class="my-4 flex justify-center {{ class }}">
<blockquote
class="twitter-tweet"
lang="en"
{% if not data_cards %}data-cards="hidden"{% endif %}
{% if no_parent %}data-conversation="none"{% endif %}
>
{{ content|raw }}
</blockquote>
</div>

View file

@ -0,0 +1,9 @@
<div class="{{ classes }}">
<iframe
src="https://www.youtube.com/embed/{{ video.id }}"
height="{{ video.attr.height }}"
width="{{ video.attr.width }}"
frameborder="0"
allowfullscreen
></iframe>
</div>

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="{{ site.locale|default('en') }}">
<head>
<link type="text/css" rel="stylesheet" href="/build/app.css"/>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300&display=swap" rel="stylesheet">
{% include 'meta' with { page, site } only %}
{% include 'plausible' with {
plausible: site.plausible,
} only %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,29 @@
{% extends 'app' %}
{% block body %}
<div class="min-h-screen font-sans text-base font-light text-gray-900 md:text-xl dark:text-white dark:bg-gray-900">
<a class="sr-only focus:not-sr-only" href="#main-content">Skip to main content</a>
{% include 'navbar' %}
<div class="py-10 px-4 mx-auto max-w-2xl md:py-10">
<div id="main-content">
<h1 class="text-xl font-bold md:text-2xl">
{% block page_title %}
{{ page.title }}
{% endblock %}
</h1>
<div class="mt-4">
{% block content_wrapper %}
{% block content %}{% endblock %}
{% endblock %}
</div>
</div>
<div class="mt-20 mb-6">
{% include 'footer' %}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1 @@
{% extends 'base' %}

View file

@ -0,0 +1,11 @@
{% extends 'base' %}
{% block content_wrapper %}
<div class="markdown">
{% if page.intro_text %}
<p class="max-w-lg">{{ page.intro_text }}</p>
{% endif %}
{{ parent() }}
</div>
{% endblock %}

View file

@ -0,0 +1,35 @@
{% extends 'base' %}
{% block content_wrapper %}
<div class="space-y-10">
<div class="space-y-6">
<header>
<time datetime="{{ page.date|date('Y-m-d') }}">
Posted on {{ page.date|date('jS F Y') -}}
</time>
</header>
<div>
{% include 'post/old-post-message' with {
post: page,
} only %}
</div>
<div class="markdown">
{{ parent() }}
</div>
{% include 'post/comments-questions' with {
twitter: {
name: site.twitter.name,
url: site.twitter.url
}
} only %}
</div>
{% include 'about-author' with {
avatar: site.avatar,
work: site.work,
} only %}
</div>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends 'base' %}
{% block content_wrapper %}
<div class="space-y-10">
<div class="space-y-6">
<div class="markdown">
{{ parent() }}
</div>
{% include 'talk/slides' with {
speakerdeck: page.speakerdeck,
} only %}
{% include 'talk/video' with {
video: page.video,
} only %}
{% include 'talk/event-list' with {
events: page.events|reverse,
} only %}
</div>
{% include 'about-author' with {
avatar: site.avatar,
work: site.work,
} only %}
</div>
{% endblock %}

View file

@ -0,0 +1,9 @@
---
title: Page not found
permalink: /404.html
exclude_from_sitemap: true
---
{% block javascripts %}
<script>plausible("404",{ props: { path: document.location.pathname } });</script>
{% endblock %}

View file

@ -0,0 +1,16 @@
---
title: "Blog"
use: ["posts"]
intro_text: |
This is where I publish my personal blog posts as well as technical posts
and tutorials on topics such as Drupal, PHP, Tailwind CSS, automated testing,
and systems administration.
---
<div class="mt-10">
<div class="space-y-8">
{% for post in data.posts|sort((a, b) => b.date <=> a.date) %}
{% include 'post/post-teaser' with { post } only %}
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,6 @@
---
title: Book a 1-on-1 consulting call
link: https://savvycal.com/opdavies/consulting-call
---
{{ page.link }}

View file

@ -0,0 +1,11 @@
---
title: Company Information
---
<div class="markdown" markdown="1">
Company name : Oliver Davies Ltd (previously Oliver Davies Web Development Ltd)
Registered address : 3 Westfield Close, Caerleon, Newport, NP18 3ED
Company number : 8017706
</div>

View file

@ -0,0 +1,9 @@
---
title: Contact Oliver
---
The best way to get in touch with me is via email: <a href="mailto:{{ site.email }}">{{ site.email }}</a>. I usually reply within one business day.
I'm also on [LinkedIn][linkedin].
[linkedin]: {{site.linkedin.url}}

147
website/source/_pages/cv.md Normal file
View file

@ -0,0 +1,147 @@
---
title: Oliver Paul Davies (opdavies)
---
<div class="markdown" markdown="1">
## Technical skills
- Languages: PHP, SQL, HTML, CSS, JavaScript, Bash
- CMSes and Frameworks: Drupal (6-9), Symfony (2-5), Sculpin, Tailwind CSS, Vue.js
- Tools: Git, PHPUnit, PHPStan, Behat, Jest, Docker, Ansible, Puppet, Apache, Nginx, MySQL, Jenkins, GitHub Actions
- Platforms: Acquia, Platform.sh, Pantheon, DigitalOcean, Linode
## Projects
### Inviqa websites (Lead Backend Developer and Technical Team Lead)
- Co-developed the Drupal 8 versions of the Inviqa UK and Germany websites, including a number of custom modules.
- Wrote custom migrations to migrate existing data from the legacy site.
- Solely upgraded the sites from Drupal 8 to Drupal 9.
- Technologies used: Drupal 8/9, Vue.js, Behat, PHPUnit, PHPCS, PHPStan, Platform.sh
- Links: <https://www.inviqa.com>, <https://www.inviqa.de>
### OutdoorLads website (Lead Developer, Microserve)
- Architected and co-developed a Drupal 8 and Drupal Commerce based events and membership website and management system.
- Introduced automated testing and test-driven development, resulting in over 100 tests being added.
- Contributed to the custom migration of data from the legacy system, including users, event (product), and attendance (order) information.
- Technologies used: Drupal 8, Drupal Commerce 2, PHPUnit, Tailwind CSS.
- Link: <https://www.outdoorlads.com>
### Drupal.org websites and infrastructure (Developer, Drupal Association)
- Improved Drupal.org by adding new features and fixing bugs, improving the user experience for new Drupal.org users and Drupal contributors and maintainers.
- Assisted in the upgrade of localize.drupal.org to Drupal 7 with high-profile community members.
- Tested and contributed to the responsive version of Bluecheese (the Drupal.org theme) which was launched in December 2014.
- Links: <https://www.drupal.org>, <https://events.drupal.org>, <https://jobs.drupal.org>
### Intranet for Admiral Insurance (Lead Developer, Precedent)
- Completed a Drupal development project, working closely and often on-site with Admiral's staff Developers.
- Integrated single-sign-on using LDAP, Active Directory, and NTLM.
- Technologies used: Drupal 7, Git, Linux, Apache, MySQL.
### Insurance group websites (Lead Developer, Freelance)
- Developed and maintained a collection of business-to-consumer and business-to-business websites, selling insurance policies for electronic gadgets.
- Increased revenue by increasing the number of sites from one to seven, and maintaining costs by re-using the same codebase and hosting and implementing patterns such as feature flags.
- Technologies used: Drupal 7, Drupal Commerce, AngularJS, PHPCS, PHPStan, Jest, GitHub, Acquia.
## Work Experience
### Lead Software Developer at Transport for Wales - 2021 to present
### Freelance Software Developer and Systems Administrator - 2007 to present
- Developed and re-developed applications using Drupal, Symfony and Silex.
- Migrated sites to newer versions of Drupal (6 to 7, and 7 to 8).
- Provisioned and maintained Linux servers for clients.
- Introduced version control systems, automation and deployment processes to existing projects.
### Senior Software Engineer at Inviqa (Remote) - 2019 to 2021
- Worked on a development team responsible for a number of Drupal 7 and 8 projects, which I led for over a year in an acting Technical Team Lead role.
- Augmented onto a client development team for five months, co-developing an application using Drupal, Angular, and TypeScript.
- Part of the out-of-hours critical application support team, supporting applications including Drupal, Magento, and Sylius.
- Certified Mental Health First Aider and part of the Wellbeing team.
- Co-organised and presented at the internal Drupal community of practice (CoP) sessions, and presented at the Engineering and Front-End CoPs.
- Link: <https://www.inviqa.com>
### Senior Drupal Developer at Microserve (Bristol, UK) - 2017 to 2019
- Developed and maintained various Drupal 7 and 8 projects including custom modules and automated tests, such as integrating Drupal with third-party services.
- Improved the accessibility of various projects by working on front-end and theming related tasks.
- Wrote custom migrations to import data from various sources into Drupal 8.
- Link: <https://microserve.io>
### Senior Drupal Developer at Appnovation (Cardiff, UK) - 2016 to 2017
- Co-developed various Drupal 7 and 8 projects for UK, US and Canadian clients, including custom modules and themes.
- Link: <https://www.appnovation.com>
### Lead Drupal Developer at CTI Digital (Remote) - 2015 to 2016
- Contributed to and helped support various existing Drupal 7 projects.
- Improved my Drupal 8 knowledge via self-guided learning, focussing on custom module development and data migration.
- Link: <https://www.ctidigital.com>
### Senior Drupal Developer at Microserve (Bristol, UK) - 2015
- Full-stack Drupal 7 development, focussing on custom module development, REST server integration via restws module, and data migration from Drupal 6.
- Updated and modernised a non-Drupal PHP platform to ensure its compatibility with their new Drupal 7 website, adding Composer to manage dependencies and Guzzle to perform HTTP requests to Drupal to trigger actions via REST.
- Link: <https://microserve.io>
### Drupal.org Developer at the Drupal Association (Remote) - 2014 to 2015
- Worked on the Drupal.org website, its sub-sites and infrastructure, developing new tools and features whilst fixing bugs and issues.
- Monitored and maintained the Drupal.org testbot infrastructure during high traffic periods, ensuring that automated tests are able to run for patches submitted to Drupal.org.
- Fixed any issues that made Drupal.org a Drupal 8 release blocker, ensuring that there were no further delays in releasing Drupal 8.
- Link: <https://www.drupal.org/association>
### Senior Drupal Developer at Precedent (Cardiff, UK) - 2013 to 2014
- Led a development team consisting of colleagues in other offices as well as an off-site client contractor.
- Completed an in-progress Drupal 7 project, adding missing functionality and fixing bugs.
### Application Developer & System Administrator at Nomensa (Bristol, UK) - 2012 to 2013
- Developed custom Drupal 7 modules including an integration with CiviCRM.
- Completed front-end theme development work with a focus on accessibility, ensuring that it was WCAG 2.0 compliant.
- Worked alongside and mentored front-end Developers in Drupal theming on projects. Several of them would later become full-time Drupal Developers and Themers.
- Provisioned servers with a Nginx, PHP-FPM and MySQL stack, and deployed applications.
- Link: <https://www.nomensa.com>
### PHP Developer at Proctor & Stevenson (Bristol, UK) - 2011 to 2012
- Developed and co-developed new websites, including the agencys first Drupal 7 client project.
- Provided ongoing support and maintenance of websites for existing clients.
- Architected and developed a Drupal 6 and Ubertcart project for a water services company, which processed residential and commercial property transactions until September 2020.
- Link: <https://www.proctors.co.uk>
### Web Developer (PHP, Drupal) at Horse & Country TV (Cwmbran, UK) - 2010 to 2011
- Maintained and supported the companys Drupal 6 website as part of a two-person team.
- Re-architected and re-developed the Events section, adding Ubercart for paid events which added a new revenue stream for the company.
- Link: <https://horseandcountry.tv>
## Certifications and Qualifications
- 2021: Platform.sh partner certification (for Inviqa)
- 2019: Adult Mental Health First Aider - Training in Mind / St. John's Ambulance
- 2018: Acquia certified Cloud Pro
- 2017: Acquia certified Front End Specialist - Drupal 8
- 2017: Acquia certified Back End Specialist - Drupal 8
- 2017: Acquia certified Developer - Drupal 8
- 2007-2009: HNC Computing (End User Support)
## Community
- Organiser of the PHP South Wales user group.
- Past organiser of the PHP South West, and Drupal Bristol user groups.
- Co-founder of the DrupalCamp Bristol conference.
- Open source contributor and maintainer.
- Drupal core contributor, and contribution mentor.
- Coding Fellowship Bootcamp mentor.
</div>

View file

@ -0,0 +1,64 @@
---
title: Oliver Davies - PHP Developer and Drupal Specialist
---
<div class="markdown" markdown="1">
I'm a long-time Web Developer and consultant. Ive led, delivered, and
maintained PHP, Drupal, and Drupal Commerce based websites, have worked for
some of the UKs largest and well-known PHP and Drupal agencies, and even for
the Drupal Association - the nonprofit organisation behind the Drupal project -
where I was employed to work on and improve the Drupal.org websites.
<a href="mailto:{{ site.email }}">Send me an email</a> to discuss your project.
## My Drupal Experience
I have contributed code to Drupal core and to various other Drupal modules, and
maintain modules and themes like Override Node Options which is used on over 30,
000 Drupal sites according to Drupal.org. Ive been a mentor at various
in-person events, helping new contributors to the Drupal project, and regularly
write blog posts, present talks and workshops, and create videos and live
streams.
As well as Drupal, Ive worked with other PHP projects like Symfony and Laravel,
static site generators like Sculpin and Jekyll, and JavaScript frameworks such
as Vue.js and Angular.
## Certifications
- Acquia certified Developer - Drupal 8 (2017)
- Acquia certified Back-End Specialist - Drupal 8 (2017)
- Acquia certified Front-End Specialist - Drupal 8 (2017)
- Acquia certified Cloud Pro (2018)
- Platform.sh Gold partner certification (2021, for Inviqa)
## Community contributions
- Authored an article on Drupal development using distributions for Linux
Journal's Drupal issue.
- Mentored new contributors at DrupalCon contribution days with their first
patches to Drupal core.
- Organised the Drupal Bristol and PHP South West (PHPSW) user groups, and
the DrupalCamp Bristol conference.
- Currently organise and sponsor the
[PHP South Wales user group](https://www.phpsouthwales.uk).
- Board member for the
[Drupal England and Wales Association](https://drupal-england-wales.github.io)
(2020 to present).
- Selecting sessions for DrupalCon Europe 2021 as part of the DrupalCon track
team.
- Mentored students on the DrupalEasy
[Drupal Career Online](https://www.drupaleasy.com/academy/dco/course-information)
course.
- Currently writing "Test-Driven Drupal", an eBook about automated testing and
test-driven development in Drupal.
## Podcasts
I've been a guest on a number of podcasts, including [Talking Drupal](https://talkingdrupal.com),
[How to Code Well](https://howtocodewell.fm), [That Podcast](https://thatpodcast.io),
and [Voices of the ElePHPant](https://voicesoftheelephpant.com), where I've
discussed topics including PHP, Drupal, CSS frameworks, and automated
testing.
</div>

View file

@ -0,0 +1,79 @@
---
title: Introduction to Automated Testing and Test-Driven Development with Drupal
drupal_versions: [9]
prices:
early: 395.00
full: 495.00
early: true
next_date: 2022-04-04
testimonials:
-
name: Scott Euser, Head of Web Development
image: /images/scott-euser.jpg
text: |
Oliver really knows his stuff. Whether you are just starting out or looking to take your knowledge to the next level, his patient and clear way of explaining will help get you there.
---
Are you a Drupal Developer who wants to learn about automated testing and test-driven development, or do you manage a development team that you'd like to train?
I've delivered large Drupal projects using automated tests and test-driven development for custom functionality, and maintain Drupal modules with thousands of installations whilst using their tests to ensure working code and prevent regressions.
I offer an interactive full-day workshop (previously presented at DrupalCamp London, and remotely for DrupalCamp NYC) that provides an introduction to automated testing in Drupal and how to utilise test-driven development - which I've updated specifically for Drupal {{ page.drupal_versions|join(' and ') }}.
## Contents
* What is automated testing, and why write tests?
* What types of tests are available in Drupal?
* Outside-in vs. inside-out testing.
* Configuring Drupal and PHPUnit to run tests locally.
* Exercise: writing tests for existing Drupal core functionality.
* Exercise: adding tests to an existing custom module.
* What is test-driven development?
* Exercise: writing a new Drupal module from scratch with test-driven development.
* Q&A
{% include "horizontal-rule" %}
## Dates and prices
The workshop is currently only available remotely, and the next available date is <span class="font-bold">{{ page.next_date|date('jS F Y') }}</span>.
Seats are available at {% if page.early %}an <span class="font-bold">early bird price of £{{ page.prices.early }}</span>{% else %}<span class="font-bold">£{{ page.prices.full }}</span>{% endif %}, with a 10% discount for bulk orders of 5 or more seats.
<div class="mt-6">
{% embed 'link-button' with {
arrow: true,
href: 'https://buy.stripe.com/6oE3cW4Su7DA1t6144',
size: 'normal',
} only %}
{% block text 'Book your seat' %}
{% endembed %}
</div>
{% include "horizontal-rule" %}
## Testimonials
{% for testimonial in page.testimonials %}
<div>
<blockquote class="mt-4">
{{ testimonial.text }}
</blockquote>
<footer class="flex items-center space-x-4 space-x-reverse">
<span class="text-base">{{ testimonial.name }}</span>
<span class="order-first">
<img
class="w-10 h-10 rounded-full border"
src="{{ testimonial.image }}"
/>
</span>
</footer>
</div>
{% endfor %}
<div class="mt-8">
{% include 'about-author' with {
avatar: site.avatar,
work: site.work,
} only %}
</div>

View file

@ -0,0 +1,29 @@
---
title: Oliver Davies - Software Developer and Consultant, PHP and Drupal specialist
permalink: /
is_front: true
exclude_from_sitemap: true
meta:
description: Oliver Davies is a UK-based Software Developer and Consultant, specialising in Drupal, PHP, and JavaScript.
---
{% set thisYear = 'today'|date('Y') %}
{% set yearsOfExperience = thisYear - site.experience.start_year %}
<div class="markdown" markdown="1">
<div class="mb-4 w-32"><img src="{{ site.avatar.path }}" alt="Picture of Oliver" class="rounded-full border border-gray"></div>
Hi, Im Oliver. Im a Full Stack Software Consultant based in South Wales in the UK.
I architect, develop, and consult on large web applications, and work with organisations, agencies, and freelance Developers to improve their code quality by using tools and workflows such as continuous integration and deployment, automated testing, test-driven development, and static analysis.
I have {{ yearsOfExperience }} years of software development and Drupal experience, have worked for the Drupal Association, and am an <a href="https://certification.acquia.com/user/4540">Acquia-certified Drupal expert</a>. I also work with complementary technologies such as Symfony, Vue.js, TypeScript, Docker, and Ansible.
I enjoy writing and contributing open-source code which you can find on my [Drupal.org] and [GitHub] profiles.
I regularly <a href="/talks">present talks and workshops</a> at user groups and conferences and am the organiser of the <a href="https://www.phpsouthwales.uk">PHP South Wales</a> user group.
<a href="/contact">Contact me</a> if youd like any more information or to discuss a project.
</div>
[drupal.org]: {{site.drupalorg.url}}
[github]: {{site.github.url}}

View file

@ -0,0 +1,50 @@
---
title: Links
links:
-
title: Twitter
url: '%site.twitter.url%'
-
title: YouTube
url: '%site.youtube.channel.url%'
-
title: LinkedIn
url: '%site.linkedin.url%'
-
title: Drupal.org
url: '%site.drupalorg.url%'
-
title: GitHub
url: '%site.github.url%'
-
title: GitHub Gists
url: '%site.github.gist.url%'
-
title: Packagist
url: '%site.packagist.url%'
-
title: Speakerdeck
url: '%site.speakerdeck.url%'
-
title: PHP South Wales
url: https://www.phpsouthwales.uk
---
<div class="max-w-md mx-auto">
<ul class="p-0 list-none space-y-4">
{% for link in page.links %}
<li>
<a
class="
w-full p-2 block border text-center no-underline text-black transition ease-in-out duration-200
hover:text-white hover:bg-blue-primary focus:text-white focus:bg-blue-primary
dark:text-white dark:hover:text-black dark:focus:text-black dark:hover:bg-white dark:focus:bg-white
"
href="{{ link.url }}?utm_source=oliverdavies.uk&utm_medium=links"
>
{{ link.title }}
</a>
</li>
{% endfor %}
</ul>
</div>

View file

@ -0,0 +1,17 @@
---
title: Pair program with me
---
I enjoy pair and mob (group) programming, so as well as [traditional freelance
services][0], I offer paid remote pair programming sessions where I'll work
with you on your own project via a Zoom call.
My experience is based around PHP, Drupal, Symfony, Vue.js, Tailwind CSS,
Ansible, Docker, clean code, automated testing, and test-driven development.
I also offer free sessions for open source projects.
To arrange a pairing session, [find an available time on my calendar][1].
[0]: /drupal-php-developer
[1]: {{site.savvycal.url}}

View file

@ -0,0 +1,14 @@
---
title: Projects
use:
- projects
draft: true
---
<ul>
{% for project in data.projects %}
<li>
<a href="{{ project.url }}">{{ project.title }}</a>
</li>
{% endfor %}
</ul>

View file

@ -0,0 +1,143 @@
---
title: Recommendations
recommendations:
-
name: Ed Welsby
tagline: Senior Developer at Proctor & Stevenson
text: |
<p>Oliver was great to work with, he has a solid knowledge of the various aspects of web development and never minded helping me out with Linux commands!</p>
image: ed-welsby.png
hidden: true
-
name: Brian Healy
tagline: Director of Business Development at Tincan
text: |
<p>Oliver was fantastic to work with - pro-active and highly responsive, he worked well remotely and as part of a project team. His understanding of the project requirement(s) and ability to translate it into working code was essential and he delivered.</p>
image: brian-healy.png
-
name: Marlon Duncanson
tagline: 'Brand & Web Specialist'
text: |
<p>Oliver is a great guy and really easy to work with. He really goes the extra mile to make sure the project is done properly. I would recommend him and will not hesitate to use him again in future.</p>
image: ~
-
name: Brian Hartwell
tagline: Interactive Creative Director
text: |
<p>Oliver was great to work with. He has expert knowledge with Drupal and delivered exactly what we were looking for on time. He's understanding, friendly and easy to get along with. I would enjoy working with him again in the future.</p>
image: ~
-
name: Daniel Easterbrook
tagline: Digital Strategy Consultant
text: |
<p>Oliver is seasoned Drupal and all round highly skilled and experienced web developer. I have worked with Oliver on an important project where he was reliable, prompt and ensured strict client deadline delivery and confidentiality at all times.</p>
image: ~
-
name: James Chapman
tagline: Director at Development Done Right
text: |
<p>We used Oliver on a number of occasions throughout 2012 and I have to say we've been delighted with his work. His skills working with Drupal are excellent particularly with custom module development and we wouldnt hesitate to recommend him others.</p>
image: james-chapman.png
-
name: Léonie Watson
tagline: Director of Accessibility at Nomensa
text: |
<p>Oliver is a flexible and hardworking developer, with a terrific knowledge of Drupal. He promotes accessibility best practice within the Drupal community, and is always happy to share his knowledge with other people.</p>
image: leonie-watson.jpg
-
name: Holly Ross
tagline: Executive Director at Drupal Association
text: |
<p>Oliver has been an outstanding contributor to the Drupal Association team. He is a talented developer who writes great code and applies his curiosity and love of learning to every project. He is also a fantastic team member, who gives to the team as much as he gets.</p>
<p>Oliver is the embodiment of everything good about the Drupal community.</p>
image: holly-ross.png
-
name: Josh Mitchell
tagline: CTO at Drupal Association
text: |
<p>Oliver is a skilled Drupal developer with a passion for the Drupal community. As his direct supervisor, I was able to watch Oliver grow with the Drupal Association and contribute an amazing amount of effort and integrity to all of his work.</p>
<p>Everything we have thrown at Oliver, he has approached with an open and flexible mind that has allowed him to work on a wide range of projects and features for Drupal products.</p>
image: josh-mitchell.png
-
name: Chris Jarvis
tagline: Developer at Microserve
text: |
<p>Oliver is an amazing colleague, he's professional, full of knowledge and I could not recommend him more.</p>
image: chris-jarvis.jpg
hidden: true
-
name: Owen Phillips
tagline: Director at Operation Fitness Ltd
text: |
<p>I have been working to build and develop my website with Oliver over the last year and I couldn't recommend higher. His ideas, knowledge and completion are to a very high standard and I look forward to continuing my build with him.</p>
image: owen-phillips.jpeg
-
name: Chris Knox
tagline: Creative Director
text: |
<p>Oliver is a skilled and enthusiastic developer, always putting the clients interests first. His approach to work is diligent and confident and this makes working with him a pleasure!</p>
image: chris-knox.jpeg
-
name: Jon Hallett
tagline: Senior Systems Administrator at the University of Bristol
text: |
<p>We use Oliver for maintaining a couple of Drupal sites for which we no longer have the skills ourselves. We became aware of Oliver through his work in the Drupal community, and about a year ago we approached him to help us with the deep dive aspects of maintaining and developing Drupal sites. He's been really helpful and very responsive. Much appreciated!</p>
image: jon-hallett.jpeg
-
name: Alan Hatch
tagline: Senior Drupal Developer at Microserve
text: |
<p>I have had the pleasure of working with Oliver on several projects at Microserve. He is a natural innovator and a great mentor who inspires others to explore new technologies and approaches. He is a highly knowledgeable professional with a passion for all things Drupal and the tenacity required to get the job done well.</p>
image: alan.jpeg
-
name: Adam Cuddihy
tagline: Web Development Manager
text: |
<p>A fantastic and highly knowledgeable Drupal Developer. Oliver saved a struggling Drupal project with his wealth of Drupal experience.</p>
image: adam.jpeg
-
name: Duncan Davidson
tagline: Director at Rohallion
text: |
<p>Oliver is a pleasure to work with, and I would engage him again without hesitation. He communicates regularly, ensures that he meets requirements, and suggests improvements to the potential solutions to the brief.</p>
image: duncan.jpeg
-
name: Anonymous client
tagline: Marketing Strategist
text: |
<p>We have only worked together for a short while but I can see Oliver is a Drupal expert.</p>
<p>His technical knowledge means we have been able to make improvements to the sites we manage quickly and efficiently.</p>
<p>If we have complex issues to contend with in the future I feel confident he will be able to deal with them.</p>
-
name: "Huw Davies"
tagline: "Web Dev Manager / DevOps / Team Manager at Admiral Group Plc"
text: |
<p>I had the pleasure of working with Oliver whilst building the first version of our drupal based intranet. His knowledge of Drupal and the wider infrastructure required to run a site was really invaluable.</p>
<p>At the time, we were very new to Drupal, so it gave us a great platform to learn from and expand our own knowledge.</p>
<p>He's the only external contractor that we've kept in touch with over the years, which goes to show how much we valued his input.</p>
image: huw.jpeg
---
<div class="space-y-8">
{% for recommendation in page.recommendations|reverse if not recommendation.hidden %}
<article>
<h2>{{ recommendation.name }}</h2>
<header>{{ recommendation.tagline }}</header>
<div class="mt-4">
<div class="flex flex-col-reverse space-y-3 space-y-reverse md:flex-row md:space-y-0 md:space-x-6">
<div class="markdown">
{{ recommendation.text|raw }}
</div>
{% if recommendation.image %}
<div class="flex-shrink-0">
<img class="w-16 h-16 bg-white rounded-full border md:w-24 md:h-24 border-gray" src="/images/recommendations/{{ recommendation.image }}">
</div>
{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>

View file

@ -0,0 +1,36 @@
---
title: Speaker Information
---
<div class="markdown" markdown="1">
## Bio
<a href="https://www.oliverdavies.uk">Oliver Davies</a> (<a href="{{ site.twitter.url }}">@{{ site.twitter.name }}</a>) has been building websites since 2007, and speaking at meetups and conferences since 2012. He is a Full Stack Developer and a certified Drupal expert who also has experience developing with Symfony, Laravel, Sculpin and Vue.js, as well as with DevOps and systems administration.
He is a {{ site.work.role}} at <a href="{{ site.work.company.url }}? utm_source=oliverdavies.uk&amp;utm_medium=speaker-information"> {{ site.work.company.name }}</a>, a Drupal core contributor and mentor, and an open source and contribution advocate.
He regularly blogs and gives talks on various topics, maintains and contributes to various open source projects, and organises the PHP South Wales user group.
## Photos
- <https://www.dropbox.com/s/say1muiqedik0l4/0188395_thumb.jpg>
## Some events that Ive spoken at
- BlueConf 2019 (Cardiff, UK)
- DrupalCamp Brighton 2015
- DrupalCamp Bristol 2016
- DrupalCamp Dublin 2017
- DrupalCamp London (2014, 2015, 2016, 2017, 2019, 2020)
- DrupalCamp North 2015 (Sunderland, UK)
- DrupalCon Amsterdam 2019
- DrupalCon Europe 2020 (Online)
- Nomad PHP
- PHP North West 2017 (Manchester, UK - 10 year anniversary)
- PHP South Coast 2016 (Portsmouth, UK)
- PHP UK Conference 2018 (London, UK)
- WordCamp Bristol 2019
I also [gave a number of talks remotely](/blog/speaking-remotely-during-lockdown) for various user groups and conferences during COVID-19.
</div>

View file

@ -0,0 +1,25 @@
---
title: Talks and workshops
use: [talks]
---
<p>Starting with my first talk in September 2012, I have given
{{ get_past_talk_count(data.talks) }} presentations and workshops at various
conferences and meetups, in-person and remotely, on topics including PHP,
Drupal, automated testing, Git, CSS, and systems administration.</p>
<div class="mt-10">
<div class="space-y-8">
{% for talk in data.talks|sort((a, b) => get_last_event_date_for_talk(b) <=> get_last_event_date_for_talk(a)) %}
<article>
<h2>
<a class="dark:text-blue-400 text-blue-primary" href="{{ talk.url|trim('/', 'right') }}">
{{ talk.title }}
</a>
</h2>
<p class="mt-1">{{ talk.description }}</p>
</article>
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,53 @@
---
title: 'Test-Driven Drupal: The Book'
---
<div class="p-6 my-10 border border-gray-300 dark:bg-gray-800 dark:border-gray-700">
<p>I'm currently (in January 2022) working on this book again, and will
update this page in due course.</p>
<p class="mt-4">For now, bookmark the <a href="https://leanpub.com/test-driven-drupal">book's page on LeanPub</a> or take a look at the
<a href="https://github.com/opdavies/test-driven-drupal-app">example
application that I'm building</a> on GitHub.</p>
</div>
<div class="markdown spaced-y-4 mb-6">
<p>Having <a href="/talks/tdd-test-driven-drupal">given talks</a> and <a href="https://web.archive.org/web/20200422110605/https://drupalcamp.london/training/Automated-Testing-and-Test-Driven-Development-in-Drupal-8">workshops</a>, been a guest on podcasts and <a href="/articles/tags/testing">written articles</a> about automated testing in Drupal, Im currently in the planning phase of a book and potentially some accompanying screencasts about it, focussing on Drupal 8.</p>
<p>Im still thinking about what use-cases to cover and examples to include, but
here are some of the things Im considering:</p>
<ul>
<li>What things to test, and what not to test</li>
<li>The different types of available tests, and when to use each</li>
<li>How to write testable code</li>
<li>What happens when I run a test?</li>
<li>How to run tests in the Drupal UI</li>
<li>How to run tests with the <code>run-tests.sh</code> script</li>
<li>How to install, configure and run tests with PHPUnit in Drupal 8</li>
<li>Viewing HTML from run tests</li>
<li>How to write your first test</li>
<li>Debugging tests</li>
<li>How to organise your test files</li>
<li>Selecting the right base class and using test traits</li>
<li>Writing your own base test classes, traits and assertions</li>
<li>Managing dependencies for your tests (fields, configuration)</li>
<li>Creating users, checking access with roles and permissions</li>
<li>Creating pages and blocks with Views and testing the output</li>
<li>Creating pages with routes and controllers and testing the output</li>
<li>Testing custom plugins</li>
<li>Testing queuing items and processing queues</li>
<li>Testing sending emails</li>
<li>Testing custom Twig filters and functions
<li>Running tests as part of your continuous integration pipeline</li>
</ul>
<p>Ill most likely be publishing it via Leanpub, and will be sending free
chapters, early-bird discounts and links to screencasts and blog posts as I
write the book to subscribers of the mailing list.</p>
<p>If you have questions or would like to suggest something for me to include in
the book, please <a href="mailto:oliver@testdrivendrupal.com">contact me</a>.</p>
{% include 'test-driven-drupal-subscribe-form.html.twig' %}
</div>

View file

@ -0,0 +1,62 @@
---
title: Things you should know about PHP
---
Thanks for attending my [Things you should know about PHP](/talks/things-you-should-know-about-php) talk.
I hope that you learned some things about PHP, its ecosystem, and its communities, and if you haven't tried using PHP yet, I'd encourage you to do so.
Here are links to the resources that I mention in the talk, plus a couple of extras.
## Resources
- [Composer](https://getcomposer.org) - dependency manager
- [Drupal](https://www.drupal.org) - content management system
- [Jigsaw](https://jigsaw.tighten.co) - static site generator
- [Laravel](https://laravel.com) - framework
- [Nomad PHP](https://nomadphp.com) - online user group
- [PHP official images on Docker Hub](https://hub.docker.com/_/php)
- [PHPStan](https://phpstan.org) - static analysis tool
- [PHPUnit](https://phpunit.de) - testing framework
- [Pest](https://pestphp.com) - testing framework
- [Psalm](https://psalm.dev) - static analysis tool
- [Sculpin](khttps://sculpin.io) - static site generator
- [WordPress](https://wordpress.org) - content management system
- [php.net](https://www.php.net) - online documentation
- [php[architect]](https://www.phparch.com) - online magazine
## Books
- [Laravel: Up & Running](https://www.oreilly.com/library/view/laravel-up/9781492041207)
- [Symfony: The Fast Track](https://symfony.com/book)
## Videos
- [Codecourse](https://codecourse.com)
- [How to Code Well](https://www.howtocodewell.net)
- [Laracasts](https://laracasts.com)
- [SymfonyCasts](https://symfonycasts.com)
## Podcasts
- [How to Code Well podcast](https://howtocodewell.fm)
- [PHPUgly](https://www.phpugly.com)
- [Talking Drupal](https://talkingdrupal.com)
- [The Laravel Podcast](https://laravelpodcast.com)
- [The PHP Roundtable](https://phproundtable.com)
- [Voices of the elePHPant](https://voicesoftheelephpant.com)
{% include "horizontal-rule" %}
## Can I help?
Do you want to introduce PHP to your company or team, or add one of these tools to your existing PHP application?
I offer consulting calls and services to reduce your onboarding time and get you up and running quicker and easier.
<div class="mt-6">
{% set href = "mailto:" ~ site.email ~ "?subject=Book in my call" %}
{% embed "link-button" with { href: href, size: "normal" } only %}
{% block text "Book in your call →" %}
{% endembed %}
</div>

View file

@ -0,0 +1,43 @@
---
title: Working with me
draft: true
---
<div class="markdown" markdown="1">
## What I'm good at
- **Self-organising**
I'm a big fan of productivity processes and tools like to-do lists and checklists to manage everything.
- **Thinking out of the box.**
I try and
- **Communicating.**
You shouldn't need to ask me what I'm working on because you should already know. I keep a daily note with a list of tasks and post this somewhere at the end of the day to keep everyone in the loop, as well as posting updates to email, Slack, JIRA tickets, GitHub issues etc.
- **Processes**
- **Documentation**
I like to document things, for myself and for others. Whether it's in an issue tracking system, a wiki, a GitHub gist, a commit message, an Evernote note, or a blog post on this site, I'd rather have a document to refer back to than try and remember how I did something or why something was done in a certain way.
- **Best practices**
## What I'm not good at
-
## How to work with me
- **Tell me why.**
Don't just say "You must use the Adapter pattern", for example, but explain why something would be good and what benefits it offers. Then I can learn, and can apply it myself next time.
- **Give me timely, direct feedback.**
Good or bad, I'd rather know and can address things if needed, and I always appreciate good feedback!
</div>

View file

@ -0,0 +1,29 @@
---
title: 10 years working full time with Drupal and PHP
excerpt: 10 years ago today, I started working for Horse & Country TV in what was my full-time Drupal development role.
tags:
- drupal
- personal
- php
date: 2020-07-19
---
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">10 years ago today, I started my first full-time Web Developer job, working for <a href="https://twitter.com/HorseAndCountry?ref_src=twsrc%5Etfw">@HorseAndCountry</a> on their (at the time) <a href="https://twitter.com/hashtag/Drupal?src=hash&amp;ref_src=twsrc%5Etfw">#Drupal</a> 6 website.</p>&mdash; Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1284744784037335040?ref_src=twsrc%5Etfw">July 19, 2020</a></blockquote>
10 years ago today, I started working for [Horse & Country TV](https://horseandcountry.tv) in what was my full-time Drupal development role.
I'd been learning and working with Drupal for a couple of years prior to this, working on some personal and freelance projects, but when I was looking to move back to this area of Wales, this job on my doorstep was ideal.
Initially starting as the sole Developer before another started a few months later, I remember being very excited to see and learn how this site has been built. Some of the main things that I remember working on was re-developing the Events section and adding paid events with [Ubercart](https://www.drupal.org/project/ubercart), and expanding my module development knowledge by adding a custom block that programmatically showed the current and next programme on the channel.
As well as working with Drupal itself, it was a great opportunity to get more hands-on experience with Linux servers and to learn new tools such as [Git](https://git-scm.com) for version control.
I also remember being asked to contribute to a public issue on Drupal.org as part of the interview process to demonstrate my debugging abilities. I decided to look at [this Drupal 6 issue](https://www.drupal.org/node/753898), and posted a comment with some updated code that I then forwarded on, and then uploaded a patch to the issue queue. This is still one of my favourite approaches for interviews, and one that I've used myself since when interviewing people for roles that use open source technologies. I much prefer this to working on internal, company specific coding tests, as it gives the interviewee some real world experience and exposure to the project itself and its community, rather than just how to _use_ it.
Posting on a Drupal core issue and submitting patches was a bit scary at the time, but I think paved the way for me later contributing to core and other Drupal and open source projects. In fact, I was a Contribution Day mentor at DrupalCon Los Angeles in 2015 and helped someone get _their_ first commit to core when [a fix was committed to Drupal 8](https://git.drupalcode.org/project/drupal/commit/9cdd22c).
After this role, I've worked for various agencies working primarily with Drupal and PHP, as well as for the [Drupal Association](https://www.drupal.org/assocation) itself. Whilst in recent years I've also started working with other frameworks like Symfony and Vue.js, Drupal and PHP has always been my core specialism.
I've been very excited by the developments in both PHP and Drupal in recent versions, and I'm looking forward to the next 10 years working with them.
Thank you Horse & Country for giving me the chance to start on my full-time Drupal journey!

View file

@ -0,0 +1,84 @@
---
title: 2014
date: 2015-03-20
excerpt: A look back at 2014.
tags:
- drupal-association
- drupalcamp-london
- personal
tweets: true
---
A lot happened in 2014. Here are some of the main things that I'd like to
highlight.
## Joined the Drupal Association
This was the main thing for me this year, in May I left
[Precedent](http://precedent.com) and joined the
[Drupal Association](https://assoc.drupal.org). I work on the Engineering team,
focused mainly on [Drupal.org](https://www.drupal.org) but I've also done some
theming work on the DrupalCon [Amsterdam](http://amsterdam2014.drupal.org) and
[Latin America](http://latinamerica2015.drupal.org) websites, and some
pre-launch work on [Drupal Jobs](https://jobs.drupal.org).
Some of the tasks that I've worked on so far are:
- Fixing remaining issues from the Drupal.org Drupal 7 upgrade.
- Improving pages for
[Supporting Partners](https://www.drupal.org/supporters/partners),
[Technology Supporters](https://www.drupal.org/supporters/technology) and
[Hosting Partners](https://www.drupal.org/supporters/hosting). These
previously were manually updated pages using HTML tables, which are now
dynamic pages built with [Views](https://www.drupal.org/project/views) using
organisation nodes.
- Configuring human-readable paths for user profiles using
[Pathauto](https://www.drupal.org/project/pathauto). Only a small change, but
made a big difference to end-users.
- Migration of user data from profile values to fields, and various user profile
improvements. This was great because now we can do things like reference
mentors by their username and display their picture on your profile, as well
as show lists of peope listing a user as their mentor. This, I think, adds a
more personal element to Drupal.org because we can see the actual people and
not just a list of names on a page.
I've started keeping a list of tasks that I've been involved with on my
[Work](/work/) page, and will be adding more things as I work on them.
### Portland
I was able to travel to Portland, Oregon twice last year to meet with the rest
of the Association staff. Both times I met new people and it was great to spend
some work and social time with everyone, and it was great to have everyone
together as a team.
## My First DrupalCamp
In February, I attended [DrupalCamp London](http://2014.drupalcamplondon.co.uk).
This was my first time attending a Camp, and I managed to attend some great
sessions as well as meet people who I'd never previously met in person. I was
also a volunteer and speaker, where I talked about
[Git Flow](/blog/what-git-flow/) - a workflow for managing your Git projects.
{% include 'tweet' with {
content: '<p>Great presentation by <a href="https://twitter.com/opdavies">@opdavies</a> on git flow at <a href="https://twitter.com/search?q=%23dclondon&amp;src=hash">#dclondon</a> very well prepared and presented. <a href="http://t.co/tDINp2Nsbn">pic.twitter.com/tDINp2Nsbn</a></p>&mdash; Greg Franklin (@gfranklin) <a href="https://twitter.com/gfranklin/statuses/440104311276969984">March 2, 2014</a>'
} %}
I was also able to do a little bit of sprinting whilst I was there, reviewing
other people's modules and patches.
Attending this and [DrupalCon Prague](https://prague2013.drupal.org) in 2013
have really opened my eyes to the face-to-face side of the Drupal community, and
I plan on attending a lot more Camps and Cons in the future.
## DrupalCon Amsterdam
I was also able to travel to Holland and attend
[DrupalCon Amsterdam](https://amsterdam2014.drupal.org) along with other members
of Association staff.
## DrupalCamp Bristol
In October, we started planning for
[DrupalCamp Bristol](http://www.drupalcampbristol.co.uk). I'm one of the
founding Committee members,

View file

@ -0,0 +1,30 @@
---
title: Accessible Bristol site launched
date: 2012-11-15
excerpt:
I'm happy to report that the Accessible Bristol was launched this week, on
Drupal 7.
tags:
- accessibility
- accessible-bristol
- nomensa
---
I'm happy to announce that the
[Accessible Bristol](http://www.accessiblebristol.org.uk) website was launched
this week, on Drupal 7. The site has been developed over the past few months,
and uses the [User Relationships](http://drupal.org/project/user_relationships)
and [Privatemsg](http://drupal.org/project/privatemsg) modules to provide a
community-based platform where people with an interest in accessibility can
register and network with each other.
The site has been developed over the past few months, and uses the
[User Relationships](http://drupal.org/project/user_relationships) and
[Privatemsg](http://drupal.org/project/privatemsg) modules to provide a
community-based platform where people with an interest in accessibility can
register and network with each other.
The group is hosting a launch event on the 28th November at the Council House,
College Green, Bristol. Interested? More information is available at
<http://www.accessiblebristol.org.uk/events/accessible-bristol-launch> or go to
<http://buytickets.at/accessiblebristol/6434> to register.

View file

@ -0,0 +1,79 @@
---
title: Add a Taxonomy Term to Multiple Nodes Using SQL
date: 2010-07-07
excerpt: How to add a new taxonomy term to multiple nodes in Drupal using SQL.
tags:
- database
- drupal-6
- drupal-planet
- sequal-pro
- sql
- taxonomy
---
In preparation for my Blog posts being added to
[Drupal Planet](http://drupal.org/planet), I needed to create a new Taxonomy
term (or, in this case, tag) called 'Drupal Planet', and assign it to new
content to imported into their aggregator. After taking a quick look though my
previous posts, I decided that 14 of my previous posts were relevant, and
thought that it would be useful to also assign these the 'Drupal Planet' tag.
I didn't want to manually open each post and add the new tag, so I decided to
make the changes myself directly into the database using SQL, and as a follow-up
to a previous post -
[Quickly Change the Content Type of Multiple Nodes using SQL](/blog/change-content-type-multiple-nodes-using-sql/).
**Again, before changing any values within the database, ensure that you have an
up-to-date backup which you can restore if you encounter a problem!**
The first thing I did was create the 'Drupal Planet' term in my Tags vocabulary.
I decided to do this via the administration area of my site, and not via the
database. Then, using [Sequel Pro](http://www.sequelpro.com), I ran the
following SQL query to give me a list of Blog posts on my site - showing just
their titles and nid values.
```language-sql
SELECT title, nid FROM node WHERE TYPE = 'blog' ORDER BY title ASC;
```
I made a note of the nid's of the returned nodes, and kept them for later. I
then ran a similar query against the term_data table. This returned a list of
Taxonomy terms - showing the term's name, and it's unique tid value.
```language-sql
SELECT NAME, tid FROM term_data ORDER BY NAME ASC;
```
The term that I was interested in, Drupal Planet, had the tid of 84. To confirm
that no nodes were already assigned a taxonomy term with this tid, I ran another
query against the database. I'm using aliases within this query to link the
node, term_node and term_data tables. For more information on SQL aliases, take
a look at <http://w3schools.com/sql/sql_alias.asp>.
```language-sql
SELECT * FROM node n, term_data td, term_node tn WHERE td.tid = 84 AND n.nid = tn.nid AND tn.tid = td.tid;
```
As expected, it returned no rows.
The table that links node and term_data is called term_node, and is made up of
the nid and vid columns from the node table, as well as the tid column from the
term_data table. Is it is here that the additional rows would need to be
entered.
To confirm everything, I ran a simple query against an old post. I know that the
only taxonomy term associated with this post is 'Personal', which has a tid
value of 44.
```language-sql
SELECT nid, tid FROM term_node WHERE nid = 216;
```
Once the query had confirmed the correct tid value, I began to write the SQL
Insert statement that would be needed to add the new term to the required nodes.
The nid and vid values were the same on each node, and the value of my taxonomy
term would need to be 84.
Once this had completed with no errors, I returned to the administration area of
my Drupal site to confirm whether or not the nodes had been assigned the new
term.

View file

@ -0,0 +1,84 @@
---
title: Adding Custom Theme Templates in Drupal 7
date: 2012-04-19
excerpt: >
Today, I had a situation where I was displaying a list of teasers for news
article nodes. The article content type had several different fields assigned
to it, including main and thumbnail images. In this case, I wanted to have
different output and fields displayed when a teaser was displayed compared to
when a complete node was displayed.
tags:
- drupal
- drupal-planet
---
Today, I had a situation where I was displaying a list of teasers for news
article nodes. The article content type had several different fields assigned to
it, including main and thumbnail images. In this case, I wanted to have
different output and fields displayed when a teaser was displayed compared to
when a complete node was displayed.
I have previously seen it done this way by adding this into in a node.tpl.php
file:
```language-php
if ($teaser) {
// The teaser output.
}
else {
// The whole node output.
}
```
However, I decided to do something different and create a separate template file
just for teasers. This is done using the hook_preprocess_HOOK function that I
can add into my theme's template.php file.
The function requires the node variables as an argument - one of which is
theme_hook_suggestions. This is an array of suggested template files that Drupal
looks for and attempts to use when displaying a node, and this is where I'll be
adding a new suggestion for my teaser-specific template. Using the `debug()`
function, I can easily see what's already there.
```language-php
array (
0 => 'node__article',
1 => 'node__343',
2 => 'node__view__latest_news',
3 => 'node__view__latest_news__page',
)
```
So, within my theme's template.php file:
```language-php
/**
* Implementation of hook_preprocess_HOOK().
*/
function mytheme_preprocess_node(&$variables) {
$node = $variables['node'];
if ($variables['teaser']) {
// Add a new item into the theme_hook_suggestions array.
$variables['theme_hook_suggestions'][] = 'node__' . $node->type . '_teaser';
}
}
```
After adding the new suggestion:
```language-php
array (
0 => 'node__article',
1 => 'node__343',
2 => 'node__view__latest_news',
3 => 'node__view__latest_news__page',
4 => 'node__article_teaser',
)
```
Now, within my theme I can create a new node--article-teaser.tpl.php template
file and this will get called instead of the node--article.tpl.php when a teaser
is loaded. As I'm not specifying the node type specifically and using the
dynamic <em>\$node->type</em> value within my suggestion, this will also apply
for all other content types on my site and not just news articles.

View file

@ -0,0 +1,106 @@
---
title: Announcing the Drupal VM Generator
date: 2016-02-15
excerpt: For the past few weeks, Ive been working on a personal side project based on Drupal VM - the Drupal VM Generator.
tags:
- drupal
- drupal-planet
- drupal-vm
- drupal-vm-generator
- symfony
---
For the past few weeks, Ive been working on a personal side project based on
Drupal VM. Its called the [Drupal VM Generator][1], and over the weekend Ive
added the final features and fixed the remaining issues, and tagged the 1.0.0
release.
![](/images/blog/drupalvm-generate-repo.png)
## What is Drupal VM?
[Drupal VM][2] is a project created and maintained by [Jeff Geerling][3]. Its a
[Vagrant][4] virtual machine for Drupal development that is provisioned using
[Ansible][5].
What is different to a regular Vagrant VM is that uses a file called
`config.yml` to configure the machine. Settings such as `vagrant_hostname`,
`drupalvm_webserver` and `drupal_core_path` are stored as YAML and passed into
the `Vagrantfile` and the `playbook.yml` file which is used when the Ansible
provisioner runs.
In addition to some essential Ansible roles for installing and configuring
packages such as Git, MySQL, PHP and Drush, there are also some roles that are
conditional and only installed based on the value of other settings. These
include Apache, Nginx, Solr, Varnish and Drupal Console.
## What does the Drupal VM Generator do?
> The Drupal VM Generator is a Symfony application that allows you to quickly
> create configuration files that are minimal and use-case specific.
Drupal VM comes with an [example.config.yml file][6] that shows all of the
default variables and their values. When I first started using it, Id make a
copy of `example.config.yml`, rename it to `config.yml` and edit it as needed,
but a lot of the examples arent needed for every use case. If youre using
Nginx as your webserver, then you dont need the Apache virtual hosts. If you
are not using Solr on this project, then you dont need the Solr variables.
For a few months, Ive kept and used boilerplace versions of `config.yml` - one
for Apache and one for Nginx. These are minimal, so have most of the comments
removed and only the variables that I regularly need, but these can still be
quite time consuming to edit each time, and if there are additions or changes
upstream, then I have two versions to maintain.
The Drupal VM Generator is a Symfony application that allows you to quickly
create configuration files that are minimal and use-case specific. It uses the
[Console component][7] to collect input from the user, [Twig][8] to generate the
file, the [Filesystem component][9] to write it.
Based on the options passed to it and/or answers that you provide, it generates
a custom, minimal `config.yml` file for your project.
Heres an example of it in action:
!['An animated gif showing the interaction process and the resulting config.yml file'](/images/blog/drupalvm-generate-example-2.gif)
You can also define options when calling the command and skip any or all
questions. Running the following would bypass all of the questions and create a
new file with no interaction or additional steps.
{{ gist('24e569577ca4b72f049d', 'with-options.sh') }}
## Where do I get it?
The project is hosted on [GitHub][1], and there are installation instructions
within the [README][10].
<div class="github-card" data-github="opdavies/drupal-vm-generator" data-width="400" data-height="" data-theme="default"></div>
The recommended method is via downloading the phar file (the same as Composer
and Drupal Console). You can also clone the GitHub repository and run the
command from there. Im also wanting to upload it to Packagist so that it can be
included if you manage your projects with Composer.
Please log any bugs or feature requests in the [GitHub issue tracker][11], and
Im more than happy to receive pull requests.
If youre interested in contributing, please feel free to fork the repository
and start doing so, or contact me with any questions.
**Update 17/02/16:** The autoloading issue is now fixed if you require the
package via Composer, and this has been tagged as the [1.0.1 release][12]
[1]: https://github.com/opdavies/drupal-vm-generator
[2]: http://www.drupalvm.com
[3]: http://www.jeffgeerling.com
[4]: http://www.vagrantup.com
[5]: https://www.ansible.com
[6]: https://github.com/geerlingguy/drupal-vm/blob/master/example.config.yml
[7]: http://symfony.com/doc/current/components/console/introduction.html
[8]: http://twig.sensiolabs.org
[9]: http://symfony.com/doc/current/components/filesystem/introduction.html
[10]:
https://github.com/opdavies/drupal-vm-generator/blob/master/README.md#installation
[11]: https://github.com/opdavies/drupal-vm-generator/issues
[12]: https://github.com/opdavies/drupal-vm-generator/releases/tag/1.0.1

View file

@ -0,0 +1,192 @@
---
title: Automating Sculpin Builds with Jenkins CI
date: 2015-07-21
excerpt: How to use Jenkins to automate building Sculpin websites.
tags:
- jenkins
- sculpin
---
As part of re-building this site with [Sculpin](http://sculpin.io), I wanted to
automate the deployments, as in I wouldn't need to run a script like
[publish.sh](https://raw.githubusercontent.com/sculpin/sculpin-blog-skeleton/master/publish.sh)
locally and have that deploy my code onto my server. Not only did that mean that
my local workflow was simpler (update, commit and push, rather than update,
commit, push and deploy), but if I wanted to make a quick edit or hotfix, I
could log into GitHub or Bitbucket (wherever I decided to host the source code)
from any computer or my phone, make the change and have it deployed for me.
I'd started using [Jenkins CI](http://jenkins-ci.org) during my time at the
Drupal Association, and had since built my own Jenkins server to handle
deployments of Drupal websites, so that was the logical choice to use.
## Installing Jenkins and Sculpin
If you dont already have Jenkins installed and configured, I'd suggest using
[Jeff Geerling](http://jeffgeerling.com/) (aka geerlingguy)'s
[Ansible role for Jenkins CI](https://galaxy.ansible.com/list#/roles/440).
I've also released an
[Ansible role for Sculpin](https://galaxy.ansible.com/list#/roles/4063) that
installs the executable so that the Jenkins server can run Sculpin commands.
## Triggering a Build from a Git Commit
I created a new Jenkins item for this task, and restricted where it could be run
to `master` (i.e. the Jenkins server rather than any of the nodes).
### Polling from Git
I entered the url to the
[GitHub repo](https://github.com/opdavies/oliverdavies.uk) into the **Source
Code Management** section (the Git option _may_ have been added by the
[Git plugin](https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin) that I have
installed).
As we dont need any write access back to the repo, using the HTTP URL rather
than the SSH one was fine, and I didnt need to provide any additional
credentials.
Also, as I knew that Id be working a lot with feature branches, I entered
`*/master` as the only branch to build. This meant that pushing changes or
making edits on any other branches would not trigger a build.
![Defining the Git repository in Jenkins](/images/blog/oliverdavies-uk-jenkins-git-repo.png)
I also checked the **Poll SCM** option so that Jenkins would be routinely
checking for updated code. This essentially uses the same syntax as cron,
specifying minutes, hours etc. I entered `* * * * *` so that Jenkins would poll
each minute, knowing that I could make this less frequent if needed.
This now that Jenkins would be checking for any updates to the repo each minute,
and could execute tasks if needed.
### Building and Deploying
Within the **Builds** section of the item, I added an _Execute Shell_ step,
where I could enter a command to execute. Here, I pasted a modified version of
the original publish.sh script.
```language-bash
#!/bin/bash
set -uex
sculpin generate --env=prod --quiet
if [ $? -ne 0 ]; then echo "Could not generate the site"; exit 1; fi
rsync -avze 'ssh' --delete output_prod/ prodwww2:/var/www/html/oliverdavies.uk/htdocs
if [ $? -ne 0 ]; then echo "Could not publish the site"; exit 1; fi
```
This essentially is the same as the original file, in that Sculpin generates the
site, and uses rsync to deploy it somewhere else. In my case, `prodwww2` is a
Jenkins node (this alias is configured in `/var/lib/jenkins/.ssh/config`), and
`/var/www/html/oliverdavies.uk/htdocs` is the directory from where my site is
served.
## Building Periodically
There is some dynamic content on my site, specifically on the Talks page. Each
talk has a date assigned to it, and within the Twig template, the talk is
positoned within upcoming or previous talks based on whether this date is less
or greater than the time of the build.
The YAML front matter:
```language-yaml
---
...
talks:
- title: Test Drive Twig with Sculpin
location: DrupalCamp North
---
```
The Twig layout:
```language-twig
{% verbatim -%}
{% for talk in talks|reverse if talk.date >= now %}
{# Upcoming talks #}
{% endfor %}
{% for talk in talks if talk.date < now %}
{# Previous talks #}
{% endfor%}
{%- endverbatim %}
```
I also didnt want to have to push an empty commit or manually trigger a job in
Jenkins after doing a talk in order for it to be positioned in the correct place
on the page, so I also wanted Jenkins to schedule a regular build regardless of
whether or not code had been pushed, so ensure that my talks page would be up to
date.
After originally thinking that I'd have to split the build steps into a separate
item and trigger that from a scheduled item, and amend my git commit item
accordingly, I found a **Build periodically** option that I could use within the
same item, leaving it intact and not having to make amends.
I set this to `@daily` (the same `H H * * *` - `H` is a Jenkins thing), so that
the build would be triggered automatically each day without a commit, and deploy
any updates to the site.
![Setting Jenkins to periodically build a new version of the site.](/images/blog/oliverdavies-uk-jenkins-git-timer.png)
## Next Steps
This workflow works great for one site, but as I roll out more Sculpin sites,
I'd like to reduce duplication. I see this mainly as Ill end up creating a
separate `sculpin_build` item thats decoupled from the site that its building,
and instead passing variables such as environment, server name and docroot path
as parameters in a parameterized build.
I'll probably also take the raw shell script out of Jenkins and save it in a
text file that's stored locally on the server, and execute that via Jenkins.
This means that Id be able to store this file in a separate Git repository with
my other Jenkins scripts and get the standard advantages of using version
control.
## Update
Since publishing this post, I've added some more items to the original build
script.
### Updating Composer
```language-bash
if [ -f composer.json ]; then
/usr/local/bin/composer install
fi
```
Updates project dependencies via
[Composer](https://getcomposer.org/doc/00-intro.md#introduction) if
composer.json exists.
### Updating Sculpin Dependencies
```language-bash
if [ -f sculpin.json ]; then
sculpin install
fi
```
Runs `sculpin install` on each build if the sculpin.json file exists, to ensure
that the required custom bundles and dependencies are installed.
### Managing Redirects
```language-bash
if [ -f scripts/redirects.php ]; then
/usr/bin/php scripts/redirects.php
fi
```
I've been working on a `redirects.php` script that generates redirects from a
.csv file, after seeing similar things in the
[Pantheon Documentation](https://github.com/pantheon-systems/documentation) and
[That Podcast](https://github.com/thatpodcast/thatpodcast.io) repositories. This
checks if that file exists, and if so, runs it and generates the source file
containing each redirect.

View file

@ -0,0 +1,72 @@
---
title: Back to the future with Gits diff and apply commands
date: 2018-04-23
excerpt: How to revert files using Git, but as a new commit to prevent force pushing.
tags:
- git
---
This is one of those “theres probably already a better way to do this”
situations, but it worked.
I was having some issues this past weekend where, despite everything working
fine locally, a server was showing a “500 Internal Server” after I pushed some
changes to a site. In order to bring the site back online, I needed to revert
the site files back to the previous version, but as part of a new commit.
The `git reset` commands removed the interim commits which meant that I couldnt
push to the remote (force pushing, quite rightly, isnt allowed for the
production branch), and using `git revert` was resulting in merge conflicts in
`composer.lock` that Id rather have avoided if possible.
This is what `git log --oneline -n 4` was outputting:
```
14e40bc Change webflo/drupal-core-require-dev version
fc058bb Add services.yml
60bcf33 Update composer.json and re-generate lock file
722210c More styling
```
`722210c` is the commit SHA that I needed to go back to.
## First Solution
My first solution was to use `git diff` to create a single patch file of all of
the changes from the current point back to the original commit. In this case,
Im using `head~3` (four commits before `head`) as the original reference, I
could have alternatively used a commit ID, tag or branch name.
```
git diff head head~3 > temp.patch
git apply -v temp.patch
```
With the files are back in the former state, I can remove the patch, add the
files as a new commit and push them to the remote.
```
rm temp.patch
git add .
git commit -m 'Back to the future'
git push
```
Although the files are back in their previous, working state, as this is a new
commit with a new commit SHA reference, there is no issue with the remote
rejecting the commit or needing to attempt to force push.
## Second Solution
The second solution is just a shorter, cleaner version of the first!
Rather than creating a patch file and applying it, the output from `git diff`
can be piped straight into `git apply`.
```
git diff head~3 head | git apply -v
```
This means that theres only one command to run and no leftover patch file, and
I can go ahead and add and commit the changes straight away.

View file

@ -0,0 +1,102 @@
---
title: Building Gmail Filters with PHP
date: 2016-07-15
excerpt: How to use PHP to generate and export filters for Gmail.
tags:
- gmail
- php
promoted: true
---
Earlier this week I wrote a small PHP library called [GmailFilterBuilder][0]
that allows you to write Gmail filters in PHP and export them to XML.
I was already aware of a Ruby library called [gmail-britta][1] that does the
same thing, but a) Im not that familiar with Ruby so the syntax wasnt that
natural to me - its been a while since I wrote any Puppet manifests, and b) it
seemed like a interesting little project to work on one evening.
The library contains two classes - `GmailFilter` which is used to create each
filter, and `GmailFilterBuilder` that parses the filters and generates the XML
using a [Twig][2] template.
## Usage
For example:
```language-php
# test.php
require __DIR__ '/vendor/autoload.php';
use Opdavies\GmailFilterBuilder\Builder;
use Opdavies\GmailFilterBuilder\Filter;
$filters = [];
$filters[] = Filter::create()
->has('from:example@test.com')
->labelAndArchive('Test')
->neverSpam();
new Builder($filters);
```
In this case, an email from `example@test.com` would be archived, never marked
as spam, and have a label of "Test" added to it.
With this code written, and the GmailFilterBuilder library installed via
Composer, I can run `php test.php` and have the XML written to the screen.
This can also be written to a file - `php test.php > filters.xml` - which can
then be imported into Gmail.
## Twig Extensions
I also added a custom Twig extension that I moved into a separate
[twig-extensions][5] library so that I and other people can re-use it in other
projects.
Its a simple filter that accepts a boolean and returns `true` or `false` as a
string, but meant that I could remove three ternary operators from the template
and replace them with the `boolean_string` filter.
Before:
<div v-pre markdown="1">
```language-twig
{% verbatim %}{{ filter.isArchive ? 'true' : 'false' }}{% endverbatim %}
```
</div>
After:
<div v-pre markdown="1">
```language-twig
{% verbatim %}{{ filter.isArchive|boolean_string }}{% endverbatim %}
```
</div>
This can then be used to generate output like this, whereas having blank values
would have resulted in errors when importing to Gmail.
```language-xml
<apps:property name='shouldArchive' value='true'/>
```
## Example
For a working example, see my personal [gmail-filters][3] repository on GitHub.
## Resources
- [The GmailFilterBuilder library on Packagist][4]
- [My Gmail filters on GitHub][3]
- [My Twig Extensions on Packagist][5]
[0]: https://github.com/opdavies/gmail-filter-builder
[1]: https://github.com/antifuchs/gmail-britta
[2]: http://twig.sensiolabs.org
[3]: https://github.com/opdavies/gmail-filters
[4]: https://packagist.org/packages/opdavies/gmail-filter-builder
[5]: https://packagist.org/packages/opdavies/twig-extensions

View file

@ -0,0 +1,18 @@
---
title: 'Building oliverdavies.uk with Sculpin: Part 1 - initial setup and configuration'
excerpt: |
First part of the "Building oliverdavies.uk" series, covering the initial
Sculpin setup and configuration.
tags: [sculpin]
draft: true
---
Based on <https://github.com/opdavies/sculpin-skeleton>.
Uses <https://github.com/opdavies/docker-image-sculpin-serve>.
`app/config/sculpin_kernel.yml`:
`app/config/sculpin_site.yml`:
`app/config/sculpin_site_prod.yml`:

View file

@ -0,0 +1,37 @@
---
title: Building the new PHPSW Website
date: 2018-02-28
excerpt:
Earlier this week we had another hack night, working on the new PHPSW user
group website.
tags:
- phpsw
- symfony
- tailwind-css
has_tweets: true
---
Earlier this week we had another hack night, working on the new [PHPSW user
group][0] website.
<div class="mb-4">
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Hacking away on the new <a href="https://twitter.com/phpsw?ref_src=twsrc%5Etfw">@phpsw</a> website with <a href="https://twitter.com/DaveLiddament?ref_src=twsrc%5Etfw">@DaveLiddament</a> and <a href="https://twitter.com/kasiazien?ref_src=twsrc%5Etfw">@kasiazien</a>. <a href="https://t.co/kmfjdQSOUq">pic.twitter.com/kmfjdQSOUq</a></p>&mdash; Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/968224364129906688?ref_src=twsrc%5Etfw">February 26, 2018</a></blockquote>
</div>
Its built with Symfony so its naturally using Twig for templating. Ive become
a big fan of the utility based approach to CSS and [Tailwind CSS][1] in
particular, so Im using that for all of the styling, and using [Webpack
Encore][2] to compile all of the assets.
We have an integration with Meetup.com which were using to pull all of our
previous event data and store them as JSON files for Symfony to parse and
render, which it then uses to generate static HTML to upload onto the server.
Were in the process of populating all of the past data, but look out for a v1
launch soon. In the meantime, feel free to take a peek at our [GitHub
repository][3].
[0]: https://phpsw.uk
[1]: https://tailwindcss.com
[2]: https://github.com/symfony/webpack-encore
[3]: https://github.com/phpsw/phpsw-ng

View file

@ -0,0 +1,42 @@
---
title: Change the Content Type of Multiple Nodes Using SQL
date: 2010-07-01
excerpt:
In this post, I will be changing values within my Drupal 6 site's database to quickly change the content type of multiple nodes.
tags:
- content-types
- database
- drupal
- drupal-6
- drupal-planet
- sequel-pro
- sql
---
In this post, I will be changing values within my Drupal 6 site's database to
quickly change the content type of multiple nodes. I will be using a test
development site with the core Blog module installed, and converting Blog posts
to a custom content type called 'News article'.
**Before changing any values within the database, ensure that you have an
up-to-date backup which you can restore if you encounter a problem!**
To begin with, I created the 'News article' content type, and then used the
Devel Generate module to generate some Blog nodes.
Using [Sequel Pro](http://www.sequelpro.com), I can query the database to view
the Blog posts (you can also do this via the
[Terminal](http://guides.macrumors.com/Terminal) in a Mac OS X/Linux,
[Oracle SQL Developer](http://www.oracle.com/technology/software/products/sql/index.html)
on Windows, or directly within
[phpMyAdmin](http://www.phpmyadmin.net/home_page/index.php)):
Using an SQL 'Update' command, I can change the type value from 'blog' to
'article'. This will change every occurance of the value 'blog'. If I wanted to
only change certain nodes, I could add a 'Where' clause to only affect nodes
with a certain nid or title.
Now, when I query the database, the type is shown as 'article'.
Now, when I go back into the administration section of my site and view the
content, the content type now shows at 'News article'.

View file

@ -0,0 +1,68 @@
---
title: Checking if a user is logged into Drupal (the right way)
date: 2013-01-09
excerpt: How to check if a user is logged in by using Drupal core API functions.
tags:
- drupal
- drupal-6
- drupal-7
- drupal-planet
- php
---
I see this regularly when working on Drupal sites when someone wants to check
whether the current user is logged in to Drupal (authenticated) or not
(anonymous).
```language-php
global $user;
if ($user->uid) {
// The user is logged in.
}
```
or
```language-php
global $user;
if (!$user->uid) {
// The user is not logged in.
}
```
The better way to do this is to use the
[user_is_logged_in()](http://api.drupal.org/api/drupal/modules!user!user.module/function/user_is_logged_in/7)
function.
```language-php
if (user_is_logged_in()) {
// Do something.
}
```
This returns a boolean (TRUE or FALSE) depending or not the user is logged in.
Essentially, it does the same thing as the first example, but there's no need to
load the global variable.
A great use case for this is within a `hook_menu()` implementation within a
custom module.
```language-php
/**
* Implements hook_menu().
*/
function mymodule_menu() {
$items['foo'] = array(
'title' => 'Foo',
'page callback' => 'mymodule_foo',
'access callback' => 'user_is_logged_in',
);
return $items;
}
```
There is also a
[user_is_anonymous()](http://api.drupal.org/api/drupal/modules!user!user.module/function/user_is_anonymous/7)
function if you want the opposite result. Both of these functions are available
in Drupal 6 and higher.

View file

@ -0,0 +1,22 @@
---
title: Checkout a specific revision from SVN from the command line
date: 2012-05-23
excerpt: How to checkout a specific revision from a SVN (Subversion) repository.
tags:
- svn
- version-control
---
How to checkout a specific revision from a SVN (Subversion) repository.
If you're checking out the repository for the first time:
```language-bash
$ svn checkout -r 1234 url://repository/path
```
If you already have the repository checked out:
```language-bash
$ svn up -r 1234
```

View file

@ -0,0 +1,38 @@
---
title: Cleanly retrieving user profile data using an Entity Metadata Wrapper
excerpt: How to use Drupal 7's EntityMetadataWrapper to cleanly retrieve user profile field data.
tags:
- drupal
- drupal-7
- drupal planet
- php
date: 2021-02-23
---
Today I needed to load some Drupal user data via a [profile2](https://www.drupal.org/project/profile2) profile. When looking into this, most resources that I found suggest using this approach and calling the `profile2_load_by_user()` function directly and passing in the user object:
```php
$account = user_load(...);
$accountWrapper = new EntityDrupalWrapper('user', $account);
// or `$accountWrapper = entity_metadata_wrapper('user', $account);
$profile = profile2_load_by_user($account->value());
// or `$profile = profile2_load_by_user($account);`
$profileWrapper = new EntityDrupalWrapper('profile2', $profile);
$firstName = $profileWrapper->get('field_first_name')->value();
```
This though requires a few steps, and as I'm a fan of object-orientated code and Entity Metadata Wrappers, I wanted to find a cleaner solution.
This is my preferred method that uses method chaining. It returns the same value, is less code, and in my opinion, it's cleaner and easier to read.
```php
$firstName = $accountWrapper
->get('profile_user_basic')
->get('field_first_name')
->value();
```

View file

@ -0,0 +1,29 @@
---
title: Conditional Email Addresses in a Webform
date: 2010-05-06
excerpt:
How to send webform emails to a different email address based on another
field.
tags:
- conditional-email
- drupal-6
- drupal-planet
- webform
---
I created a new Webform to serve as a simple Contact form, but left the main
configuration until after I created the form components. I added 'Name',
'Email', 'Subject' and 'Message' fields, as well as a 'Category' select list.
Below 'Options', I entered each of my desired options in the following format:
```language-ini
Email address|Visible name
```
I went back to the form configuration page and expanded 'Conditional Email
Recipients', and selected my Category. Note that the standard 'Email To' field
above it needs to be empty. Originally, I made the mistake of leaving addresses
in that field which resulted in people being sent emails regardles of which
category was selected. I then configured the rest of the form.
Then, when I went to the finished form, the category selection was available.

View file

@ -0,0 +1,70 @@
---
title: Configuring the Reroute Email Module
date: 2014-12-22
excerpt:
How to configure the Reroute Email module, to prevent sending emails to real
users from your pre-production sites!
tags:
- drupal
- drupal-6
- drupal-7
- drupal-planet
- email
draft: true
---
[Reroute Email](https://www.drupal.org/project/reroute_email) module uses
`hook_mail_alter()` to prevent emails from being sent to users from
non-production sites. It allows you to enter one or more email addresses that
will receive the emails instead of delivering them to the original user.
> This is useful in case where you do not want email sent from a Drupal site to
> reach the users. For example, if you copy a live site to a test site for the
> purpose of development, and you do not want any email sent to real users of
> the original site. Or you want to check the emails sent for uniform
> formatting, footers, ...etc.
As we don't need the module configured on production (we don't need to reroute
any emails there), it's best to do this in code using settings.local.php (if you
have one) or the standard settings.php file.
The first thing that we need to do is to enable rerouting. Without doing this,
nothing will happen.
```language-php
$conf['reroute_email_enable'] = TRUE;
```
The next option is to whether to show rerouting description in mail body. I
usually have this enabled. Set this to TRUE or FALSE depending on your
preference.
```language-php
$conf['reroute_email_enable_message'] = TRUE;
```
The last setting is the email address to use. If you're entering a single
address, you can add it as a simple string.
```language-php
$conf['reroute_email_address'] = 'person1@example.com';
```
In this example, all emails from the site will be rerouted to
person1@example.com.
If you want to add multiple addresses, these should be added in a
semicolon-delimited list. Whilst you could add these also as a string, I prefer
to use an array of addresses and the `implode()` function.
```language-php
$conf['reroute_email_address'] = implode(';', array(
'person1@example.com',
'person2@example.com',
'person3@example.com',
));
```
In this example, person2@example.com and person3@example.com would receive their
emails from the site as normal. Any emails to addresses not in the array would
continue to be redirected to person1@example.com.

View file

@ -0,0 +1,46 @@
---
title: Continuous Integration vs Continuous Integration
excerpt: My views on the definitions of "continuous integration".
tags:
- git
date: 2021-10-07
---
![A meme with Spider-Man pointing at Spider-Man, both labelled with 'Continuous Integration'](/images/blog/continuous-integration-spiderman.jpg)
There seem to be two different definitions for the term "continuous integration" (or "CI") that I've come across whilst reading blogs, listening to podcasts, and watching video tutorials.
## Tooling
The first is around remote tools such as GitHub Actions, GitLab CI, Bitbucket Pipelines, Circle CI, and Jenkins, which automatically run tasks whenever you push or merge (or "integrate") code - such as code linting, performing static analysis checks, running automated tests, or building a deployment artifact.
These focus on code quality and replicate steps that you can run locally, ensuring that the build is successful and that if the CI checks pass then the code can be deployed.
My issue with this definition is that it may not be continuous. You could push code once a day or once a year, and it would perform the same checks and have the same outcomes and benefits.
## Workflow
The second definition isn't about tools - it's about how often you update, merge and push code (which commonly leads to feature branch vs trunk-based development, and Git Flow vs GitHub Flow discussions). How often are you pulling in the latest code, testing it with your local changes, and pushing your code for everyone else to see?
If you're using feature branches, how long do they last, and how quickly are they merged into the main branch?
Weekly? Daily? Hourly?
The workflow definition doesn't need GitHub, GitLab, or Bitbucket to run checks - it's about keeping your local code continuously (or as often as possible) updated and integrated with the remote code.
This ensures that you're developing from the latest stable version and not one that is days or weeks out of date.
This means that merge conflicts and much less common as you're always pulling in the latest code and ensuring that it can be integrated.
## Conclusion
One definition isn't dependent on the other.
You don't need the tooling and automation to use a continuous integration workflow, but I'd recommend it. It's useful to know and have confidence that the build passes, especially if you're pulling and pushing code several times a day, but it isn't a prerequisite.
If you're working on a new feature or fixing a bug, pull down the latest code,
test your changes, and push it back as often as possible.
If you watch a video, read a blog post, or listen to a podcast about continuous integration or "How to set up CI", remember that it's not just about the tooling.
There's a different workflow and mindset to consider that introduces other complementary concepts such as automated testing and test-driven development, pair and mob programming, feature flags, and continuous delivery.

View file

@ -0,0 +1,161 @@
---
title: Create a Better Photo Gallery in Drupal - Part 1
date: 2010-08-11
excerpt:
How I started converting and migrating a Coppermine photo gallery into Drupal.
tags:
- cck
- drupal
- drupal-6
- drupal-planet
- photo-gallery
- sequel-pro
- sql
- views
- views-attach
---
Recently, I converted a client's static HTML website, along with their
Coppermine Photo Gallery, into a Drupal-powered website.
Over the next few posts, I'll be replicating the process that I used during the
conversion, and how I added some additional features to my Drupal gallery.
To begin with, I created my photo gallery as described by
[Jeff Eaton](http://www.lullabot.com/about/team/jeff-eaton) in
[this screencast](http://www.lullabot.com/articles/photo-galleries-views-attach),
downloaded all my client's previous photos via FTP, and quickly added them into
the new gallery using the
[Imagefield Import](http://drupal.org/project/imagefield_import) module (which I
mentioned
[previously](/blog/quickly-import-multiples-images-using-imagefieldimport-module/)).
When I compare this to the previous gallery, I can see several differences which
I'd like to include. The first of which is the number of photos in each gallery,
and the date that the most recent photo was added.
To do this, I'd need to query my website's database. To begin with, I wanted to
have a list of all the galleries on my site which are published, and what
they're unique node ID values are. To do this, I opened Sequel Pro and entered
the following code:
```language-sql
SELECT title
AS title, nid
AS gallery_idFROM node
WHERE type = 'gallery'
AND status = 1;
```
As the nid value of each gallery corresponds with the 'field_gallery_nid' field
within the content_type_photo field, I can now query the database and retrieve
information about each specific gallery.
For example, using [aliasing](http://www.w3schools.com/sql/sql_alias.asp) within
my SQL statement, I can retrieve a list of all the published photos within the
'British Squad 2008' gallery by using the following code:
```language-sql
SELECT n.title, n.nid, p.field_gallery_nid
FROM node n, content_type_photo p
WHERE p.field_gallery_nid = 105
AND n.status = 1
AND n.nid = p.nid;
```
I can easily change this to count the number of published nodes by changing the
first line of the query to read SELECT COUNT(\*).
```language-sql
SELECT COUNT(*)
FROM node n, content_type_photo p
WHERE p.field_gallery_nid = 105
AND n.status = 1
AND n.nid = p.nid;
```
As I've used the [Views Attach](http://drupal.org/project/views_attach) module,
and I'm embedding the photos directly into the Gallery nodes, I easily add this
to each gallery by creating a custom node-gallery.tpl.php file within my theme.
I can then use the following PHP code to retrieve the node ID for that specific
gallery:
```language-php
<?php
$selected_gallery = db_result(db_query("
SELECT nid
FROM {node}
WHERE type = 'gallery'
AND title = '$title'
"));
?>
```
I can then use this variable as part of my next query to count the number of
photos within that gallery, similar to what I did earlier.
```language-php
<?php
$gallery_total = db_result(db_query("
SELECT COUNT(*)
FROM {content_type_photo}
WHERE field_gallery_nid = $selected_gallery
"));
?>
```
Next, I wanted to display the date that the last photo was displayed within each
album. This was done by using a similar query that also sorted the results in a
descending order, and limited it to one result - effectively only returning the
created date for the newest photo.
```language-php
<?php
$latest_photo = db_result(db_query("
SELECT n.created
FROM {node} n, {content_type_photo} p
WHERE p.field_gallery_nid = $selected_gallery
AND n.nid = p.nid
ORDER BY n.created DESC LIMIT 1
"));
?>
```
This was all then added into a 'print' statement which displayed it into the
page.
```language-php
<?php
if ($selected_gallery_total != 0) {
$output = '<i>There are currently ' . $selected_gallery_total . ' photos in this gallery.';
$output .= 'Last one added on ' . $latest_photo . '</i>';
print $output;
}
?>
```
OK, so let's take a look at the Gallery so far:
You will notice that the returned date value for the latest photo added is
displaying the UNIX timestamp instead of in a more readable format. This can be
changed by altering the 'print' statement to include a PHP 'date' function:
```language-php
<?php
if ($selected_gallery_total != 0) {
$output = '<i>There are currently ' . $selected_gallery_total . ' photos in this gallery.';
$output .= 'Last one added on ' . date("l, jS F, Y", $latest_photo) . '.</i>';
print $output;
}
?>
```
The values that I've entered are from
[this page](http://php.net/manual/en/function.date.php) on PHP.net, and can be
changed according on how you want the date to be displayed.
As I've added all of these photos today, then the correct dates are being
displayed. However, on the client's original website, the majority of these
photos were pubished several months or years ago, and I'd like the new website
to still reflect the original created dates. As opposed to modifying each
individual photograph, I'll be doing this in bulk in my next post.

View file

@ -0,0 +1,58 @@
---
title: Create a Better Photo Gallery in Drupal - Part 2
date: 2010-08-17
excerpt: Updating the galleries created and modified dates.
tags:
- drupal-6
- drupal-planet
- photo-gallery
- sequel-pro
- sql
---
At the end of my last post, I'd finished creating the first part of the new
photo gallery, but I wanted to change the dates of the published photos to
reflect the ones on the client's original website.
Firstly, I'll refer to the previous list of published galleries that I created
before, and create something different that also displays the created and
modified dates. Picking the node ID of the required gallery, I used the
following SQL query to display a list of photos.
```language-sql
SELECT n.title, n.nid, n.created, n.changed, p.field_gallery_nid
FROM node n, content_type_photo pWHERE n.type = 'photo'
AND p.field_gallery_nid = 103AND n.nid = p.nid
ORDER BY n.nid ASC;
```
When I look back at the old photo gallery, I can see that the previous 'last
added' date was June 27, 2008. So, how do I update my new photos to reflect that
date? Using <http://www.onlineconversion.com/unix_time.htm>, I can enter the
required date in its readable format, and it will give me the equivilent UNIX
timestamp. To keep things relatively simple, I'll set all photos within this
gallery to the same time.
The result that I'm given is '1217149200'. I can now use an UPDATE statement
within another SQL query to update the created and modified dates.
```language-sql
UPDATE node
INNER JOIN content_type_photo
ON node.nid = content_type_photo.nid
SET
node.created = 1217149200,
node.changed = 1217149200
WHERE content_type_photo.field_gallery_nid = 103
```
Now when I query the database, both the created and modified dates have been
updated, and when I return to the new photo gallery, the updated value is being
displayed.
Once the changes have been applied, it's a case of repeating the above process
for each of the required galleries.
In the next post, I'll explain how to add a count of published galleries and
photos on the main photo gallery page, as well as how to install and configure
the [Shadowbox](http://drupal.org/project/shadowbox) module.

View file

@ -0,0 +1,64 @@
---
title: Create a Better Photo Gallery in Drupal - Part 2.1
date: 2010-10-22
excerpt: The missing code to get totals of galleries and photos.
tags:
- drupal
---
Today, I realised that I hadn't published the code that I used to create the
total figures of galleries and photos at the top of the gallery (I said at the
end of
[Part 2](/blog/create-better-photo-gallery-drupal-part-2/ 'Create a Better Photo Gallery in Drupal - Part 2')
that I'd include it in
[Part 3](/blog/create-better-photo-gallery-drupal-part-3/ 'Create a Better Photo Gallery in Drupal - Part 3'),
but I forgot). So, here it is:
```language-php
<?php
// Queries the database and returns a list of nids of published galleries.
$galleries = db_query("SELECT nid FROM {node} WHERE type = 'gallery' AND status = 1");
// Resets the number of photos.
$output = 0;
// Prints a list of nids of published galleries.
while($gallery = db_fetch_array($galleries)) {
$gallery_id = $gallery['nid'];
$photos = $photos + db_result(db_query("SELECT COUNT(*) FROM node n, content_type_photo ctp WHERE n.status = 1 AND n.type = 'photo' AND ctp.field_gallery_nid = $gallery_id AND n.nid = ctp.nid"));
}
// Prints the output.
print 'There ';
if($photos == 1) {
print 'is';
}
else {
print 'are';
}
print ' currently ';
print $photos . ' ';
if($photos == 1) {
print 'photo';
}
else {
print 'photos';
}
print ' in ';
// Counts the number of published galleries on the site.
$galleries = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE TYPE = 'gallery' AND STATUS = 1"));
// Prints the number of published galleries.
print $galleries;
if ($galleries == 1) {
print ' gallery';
}
else {
print ' galleries';
}
print '.';
?>
```
It was applied to the view as a header which had the input format set to PHP
code.

View file

@ -0,0 +1,49 @@
---
title: Create a Better Photo Gallery in Drupal - Part 3
date: 2010-10-13
excerpt: Grouping galleries by category.
tags:
- drupal
---
The next part of the new gallery that I want to implement is to group the
galleries by their respective categories. The first step is to edit my original
photo_gallery view and add an additional display.
I've called it 'Taxonomy', and it's similar to the original 'All Galleries'
view. The differences are that I've added the taxonomy term as an argument,
removed the header, and updated the path to be `gallery/%`. The other thing that
I need to do is overwrite the output of the original 'All Galleries' View by
creating a file called `views-view--photo-gallery--page-1.tpl.php` and placing
it within my theme directory.
Within that file, I can remove the standard content output. This still outputs
the heading information from the original View. I can now use the function
called 'views_embed_view' to embed my taxonomy display onto the display. The
views_embed_view function is as follows:
```language-php
<?php views_embed_view('my_view', 'block_1', $arg1, $arg2); ?>
```
So, to display the galleries that are assigned the taxonomy of 'tournaments', I
can use the following:
```language-php
<?php print views_embed_view('photo_gallery', 'page_2', 'tournaments'); ?>
```
To reduce the amount of code needed, I can use the following 'while' loop to
generate the same code for each taxonomy term. It dynamically retrieves the
relevant taxonomy terms from the database, and uses each name as the argument
for the view.
```language-php
<?php
$terms = db_query("SELECT * FROM {term_data} WHERE vid = 1");
while ($term = db_fetch_array($terms)) {
print '<h3>' . $term['name'] . '</h3>';
print views_embed_view('gallery', 'page_2', $term['name']);
}
?>
```

View file

@ -0,0 +1,44 @@
---
title: Create a Block of Social Media Icons using CCK, Views and Nodequeue
date: 2010-06-23
excerpt: How to create a block of social media icons in Drupal.
tags:
- drupal
- drupal-6
- drupal-planet
- nodequeue
- oliverdavies.co.uk
- views
---
I recently decided that I wanted to have a block displayed in a sidebar on my
site containing icons and links to my social media profiles -
[Twitter](http://twitter.com/opdavies), [Facebook](http://facebook.com/opdavies)
etc. I tried the [Follow](http://drupal.org/project/follow) module, but it
lacked the option to add extra networks such my
[Drupal.org](http://drupal.org/user/381388) account, and my
[RSS feed](http://oliverdavies.co.uk/rss.xml). I started to create my own
version, and then found
[this Blog post](http://www.hankpalan.com/blog/drupal-themes/add-your-social-connections-drupal-icons)
by Hank Palan.
I created a 'Social icon' content type with the body field removed, and with
fields for a link and image - then downloaded the favicons from the appropriate
websites to use.
However, instead of using a custom template (node-custom.tpl.php) file, I used
the Views module.
I added fields for the node titles, and the link from the node's content. Both
of these are excluded from being displayed on the site. I then re-wrote the
output of the Icon field to create the link using the URL, and using the node's
title as the image's alternative text and the link's title.
I also used the [Nodequeue](http://drupal.org/project/nodequeue) module to
create a nodequeue and arrange the icons in the order that I wanted them to be
displayed. Once this was added as a relationship within my View, I was able to
use node's position in the nodequeue as the sort criteria.
To complete the process, I used the
[CSS Injector](http://drupal.org/project/css_injector) module to add some
additional CSS styling to position and space out the icons.

View file

@ -0,0 +1,71 @@
---
title: Create a Flickr Photo Gallery Using Feeds, CCK and Views
date: 2010-06-28
excerpt:
In this tutorial, I'll show you how to create a photo gallery which uses
photos imported from Flickr.
tags:
- drupal-planet
- drupal-6
- photo-gallery
- views
- cck
- imagecache
- feeds
- filefield
- flickr
- imagefield
---
In this tutorial, I'll show you how to create a photo gallery which uses photos
imported from [Flickr](http://www.flickr.com).
The modules that I'll use to create the Gallery are:
- [CCK](http://drupal.org/project/cck)
- [Feeds](http://drupal.org/project/feeds)
- [Feeds Image Grabber](http://drupal.org/project/feeds_imagegrabber)
- [FileField](http://drupal.org/project/filefield)
- [ImageAPI](http://drupal.org/project/imageapi)
- [ImageCache](http://drupal.org/project/imagecache)
- [ImageField](http://drupal.org/project/imagefield)
- [Views](http://drupal.org/project/views)
The first thing that I did was to create a content type to store my imported
images. I named it 'Photo', removed the Body field, and added an Image field.
Next, I installed and configured the Feeds and Image Grabber module. I used an
overridden default Feed to import my photos from Flickr using the following
settings:
- **Basic settings:** I changed the Refresh time to 15 minutes.
- **Processor settings:** I changed the content type to 'Photo', and the
author's name from 'anonymous'.
- **Processor mapping:** I added a new mapping from 'Item URL (link)' to 'Photo
(FIG)'. The Photo FIG target is added by the Image Grabber module.
Next, I needed to create the actual Feed, which I did by clicking 'Import'
within the Navigation menu, and clicking 'Feed'. I gave it a title, entered the
URL to my RSS feed from Flickr, and enabled the Image Grabber for this feed.
Once the Feed is created, the latest 20 images from the RSS feed are imported
and 20 new Photos nodes are created. In the example below, the image with the
'Photo' label is the Image field mapped by the Image Grabber module. It is this
image that I'll be displaying within my Gallery.
With the new Photo nodes created, I then created the View to display them.
The View selects the image within the Photo content type, and displays in it a
grid using an ImageCache preset. The View is limited to 20 nodes per page, and
uses a full pager if this is exceeded. The nodes are sorted by the descending
post date, and filtered by whether or not they are published, and only to
include Photo nodes.
As an additional effect, I also included the 'Feeds Item - Item Link' field,
which is basically the original link from the RSS feed. By checking the box the
exclude the item from the display, it is not shown, but makes the link available
to be used elsewhere. By checking the box 'Re-write the output for this field'
on the 'Content: Photo' field, I was able to add the replacement token (in this
case, [url]) as the path for a link around each image. This meant that when
someone clicked a thumbnail of a photo, they were directed to the Flickr website
instead of the node within my Drupal site.

View file

@ -0,0 +1,59 @@
---
title: Create Multigroups in Drupal 7 using Field Collections
date: 2011-08-28
excerpt:
How to replicate CCKs multigroups in Drupal 7 using the Field Collections
module.
tags:
- cck
- drupal-7
- drupal-planet
- entity-api
- field-collection
- fields
- multigroup
---
One of my favourite things lately in Drupal 6 has been CCK 3, and more
specifically, the Content Multigroups sub-module. Basically this allows you to
create a fieldset of various CCK fields, and then repeat that multiple times.
For example, I use it on this site whist creating invoices for clients. I have a
fieldset called 'Line Item', containing 'Description', 'Quantity' and 'Price'
fields. With a standard fieldset, I could only have one instance of each field -
however, using a multigroup, I can create multiple groups of line items which I
then use within the invoice.
But at the time of writing this, there is no CCK 3 version for Drupal 7. So, I
created the same thing using
[Field Collection](http://drupal.org/project/field_collection) and
[Entity](http://drupal.org/project/entity) modules.
With the modules uploaded and enabled, go to admin/structure/field-collections
and create a field collection.
With the module enabled, you can go to your content type and add a Field
Collection field. By default, the only available Widget type is 'Hidden'.
Next, go to admin/structure/field-collections and add some fields to the field
collection - the same way that you would for a content type. For this collection
is going to contain two node reference fields - Image and Link.
With the Field Collection created, I can now add it as a field within my content
type.
Whilst this works perfectly, the field collection is not editable from the node
edit form. You need to load the node, and the collection is displayed here with
add, edit, and delete buttons. This wasn't an ideal solution, and I wanted to be
able to edit the fields within the collection from the node edit form - the same
way as I can using multigroups in Drupal 6.
After some searching I found
[a link to a patch](http://drupal.org/node/977890#comment-4184524) which when
applied adds a 'subform' widget type to the field collection field and allows
for it to be embedded into, and editable from within the node form. Going back
to the content type fields page, and clicking on 'Hidden' (the name of the
current widget), I can change it to subform and save my changes.
With this change applied, when I go back to add or edit a node within this
content type, my field collection will be easily editable directly within the
form.

View file

@ -0,0 +1,62 @@
---
title: Create an Omega Subtheme with LESS CSS Preprocessor using Omega Tools and Drush
date: 2012-04-16
excerpt: How to create an Omega subtheme on the command line using Drush.
tags:
- drupal
- drupal-7
- drupal-planet
- less
- omega
- theming
---
In this tutorial I'll be showing how to create an
[Omega](http://drupal.org/project/omega) subtheme using the
[Omega Tools](http://drupal.org/project/omega_tools) module, and have it working
with the [LESS CSS preprocessor](http://lesscss.org).
The first thing that I need to do is download the Omega theme and the Omega
Tools and [LESS](http://drupal.org/project/less 'LESS module on drupal.org')
modules, and then to enable both modules. I'm doing this using Drush, but you
can of course do this via the admin interface at admin/modules.
```language-bash
$ drush dl less omega omega_tools;
$ drush en -y less omega_tools
```
With the Omega Tools module enabled I get the drush omega-subtheme command that
creates my Omega subtheme programatically. Using this command, I'm creating a
new subtheme, enabling it and setting it as the default theme on my site.
```language-bash
$ drush omega-subtheme "Oliver Davies" --machine_name="oliverdavies" --enable --set-default
```
By default, four stylesheets are created within the subtheme's css directory.
The first thing that I'm going to do is rename `global.css` to `global.less`.
```language-bash
$ mv css/global.css css/global.less
```
Now I need to find all references to global.css within my oliverdavies.info
file. I did this using `$ nano oliverdavies.info`, pressing `Ctrl+W` to search,
then `Ctrl+R` to replace, entering `global.css` as the search phrase, and then
`global.less` as the replacement text. After making any changes to
oliverdavies.info, I need to clear Drupal's caches for the changes to be
applied.
```language-bash
$ drush cc all
```
I tested my changes by making some quick additions to my global.less file and
reloading the page.
If your changes aren't applied, then confirm that your global.less file is
enabled within your theme's configuration. I did this by going to
admin/appearance/settings/oliverdavies, clicking on the Toggle styles tab within
_Layout configuration_ and finding global.less at the bottom of _Enable optional
stylesheets_.

View file

@ -0,0 +1,50 @@
---
title: Create a Slideshow of Multiple Images Using Fancy Slide
date: 2010-05-25
excerpt: How to create a slideshow of images using Drupals Fancy Slide module.
tags:
- drupal
- drupal-6
- drupal-planet
- fancy-slide
- slideshow
---
Whilst updating my About page, I thought about creating a slideshow of several
images instead of just the one static image. When I looking on Drupal.org, the
only slideshow modules were to create slideshows of images that were attached to
different nodes - not multiple images attached to one node. Then, I found the
[Fancy Slide](http://drupal.org/project/fancy_slide) module. It's a jQuery
Slideshow module with features that include integration with the
[CCK](http://drupal.org/project/cck),
[ImageCache](http://drupal.org/project/imagecache) and
[Nodequeue](http://drupal.org/project/nodequeue) modules.
I added an CCK Image field to my Page content type, and set the number of values
to 3, then uploaded my images to the Page.
Whilst updating my About page, I thought about creating a slideshow of several
images instead of just the one static image. When I looking on Drupal.org, the
only slideshow modules were to create slideshows of images that were attached to
different nodes - not multiple images attached to one node. Then, I found the
[Fancy Slide](http://drupal.org/project/fancy_slide) module. It's a jQuery
Slideshow module with features that include integration with the
[CCK](http://drupal.org/project/cck),
[ImageCache](http://drupal.org/project/imagecache) and
[Nodequeue](http://drupal.org/project/nodequeue) modules. Once the Images were
added, I went to the Fancy Slide settings page and created the slideshow.
I added the dimensions of my images, the type of animation, specified the node
that contained the images, the slideshow field, delay between slides and
transition speed. With the slideshow created, it now needed embedding into the
page.
I added the following code into my About page, as described in the Fancy Slide
readme.txt file - the number representing the ID of the slideshow.
```language-php
<?php print theme('fancy_slide', 1); ?>
```
In my opinion, this adds a nice effect to the About page. I like it because it's
easy to set up, and easy to add additional images later on if required.

View file

@ -0,0 +1,50 @@
---
title: Create Virtual Hosts on Mac OS X Using VirtualHostX
date: 2010-07-02
excerpt:
How to use the VirtualHostX application to manage virtual hosts on Mac OS X.
tags:
- drupal-6
- drupal-planet
- mamp
- virtual-hosts
- virtualhostx
---
This isn't a Drupal related topic per se, but it is a walk-through of one of the
applications that I use whilst doing Drupal development work. I assume, like
most Mac OS X users, I use [MAMP](http://www.mamp.info/en/index.html) to run
Apache, MySQL and PHP locally whilst developing. I also use virtual hosts in
Apache to create local .dev domains which are as close as possible to the actual
live domains. For example, if I was developing a site called mysite.com, my
local development version would be mysite.dev.
Normally, I would have to edit the hosts file and Apache's httpd.conf file to
create a virtual host. The first to set the domain and it's associated IP
address, and the other to configure the domain's directory, default index file
etc. However, using [VirtualHostX](http://clickontyler.com/virtualhostx), I can
quickly create a virtual host without having to edt any files. Enter the virtual
domain name, the local path and the port, and apply the settings. VirtualHostX
automatically restarts Apache, so the domain is ready to work straight away. You
can also enter custom directives from within the GUI.
There's also an option to share the host over the local network. Next, I intend
on configuring a virtual Windows PC within VMware Fusion to view these domains
so that I can do cross-browser testing before putting a site live.
I ensured that my Apache configuration within MAMP was set to port 80, and that
VirtualHostX was using Apache from MAMP instead of Apple's built-in Apache.
**Note:** One problem that I had after setting this up, was that I was receving
an error when attempting to open a Drupal website which said _'No such file or
directory'._
After some troubleshooting, I found out that Web Sharing on my Mac had become
enabled (I don't know why, I've never enabled it), and that this was causing a
conflict with Apache. Once I opened my System Preferences and disabled it,
everything worked fine!
This, along with [MAMP](http://www.mamp.info/en/index.html),
[Coda](http://www.panic.com/coda), [Sequel Pro](http://www.sequelpro.com), and
[Transmit](http://www.panic.com/transmit), has become an essential tool within
my development environment.

View file

@ -0,0 +1,40 @@
---
title: Create a Zen Sub-theme Using Drush
date: 2013-09-06
excerpt: How to quickly create a Zen sub-theme using Drush.
tags:
- drupal
- drupal-planet
- drush
- zen
- theming
---
How to use [Drush](https://drupal.org/project/drush) to quickly build a new
sub-theme of [Zen](https://drupal.org/project/zen).
First, download the [Zen](https://drupal.org/project/zen 'The Zen theme') theme
if you haven't already done so.
```language-bash
$ drush dl zen
```
This will now enable you to use the "drush zen" command.
```language-bash
$ drush zen "Oliver Davies" oliverdavies --description="A Zen sub-theme for oliverdavies.co.uk" --without-rtl
```
The parameters that I'm passing it are:
1. The human-readable name of the theme.
2. The machine-readable name of the theme.
3. The description of the theme (optional).
4. A flag telling Drush not to include any right-to-left elements within my
sub-theme as these aren't needed (optional).
This will create a new theme in sites/all/themes/oliverdavies.
For further help, type `$ drush help zen` to see the Drush help page for the zen
command.

View file

@ -0,0 +1,82 @@
---
title: Creating a custom PHPUnit command for DDEV
excerpt: How to create a custom command to run PHPUnit commands in DDEV.
tags:
- ddev
- drupal
- drupal-planet
- php
date: 2020-08-28
---
To begin with, let's create an empty file for our command:
```bash
touch .ddev/commands/web/phpunit
```
Commands are located within the `.ddev/commands` directory, with a sub-directory for the container name in which the command should be executed - or `host` if it's a command that is to be run on the host machine.
As [the example repo](https://github.com/opdavies/ddev-phpunit-command-example) has a `web` sub-directory to mimic my Drupal application structure, the command should be run inside the web container so the file should be placed within the `.ddev/commands/web` directory.
As we want the command to be 'phpunit', the filename should also be `phpunit`.
This is an example of a basic command, which is a simple bash script:
```bash
#!/usr/bin/env bash
echo 'running phpunit...'
```
To begin with, let's echo some simple text to check that the command is working. It should also be listed if you run the `ddev` command.
To check the working directory that it used when the command is run, add the following line in the command file:
```bash
echo $(pwd)
```
In the example, it is `/var/www/html/web`. Note that we are already inside the `web` sub-directory.
## Running PHPUnit
To run PHPUnit, I can add the following to the command file:
```
../vendor/bin/phpunit --config .. $*
```
As we're already in the `web` directory, the command needs to go up on level before running the PHPUnit command, and uses `--config` to define the path to the `phpunit.xml.dist` file which is also in the parent directory.
Using `$*` adds any additional arguments from the CLI to the command inside the container.
The command could be made simpler by overridding the `working_directory` value in `.ddev/config`:
```json
working_dir:
web: /var/www/html
```
This means that we start in `/var/www/html` rather than inside the `web` directory, and that we can simplify the command to be:
```
vendor/bin/phpunit $*
```
Because the `phpunit.xml.dist` file is inside the working directory, I no longer need to specify its path.
## Adding documentation
To add documentation and help text to the command, add these lines to the command file:
```bash
## Description: Run PHPUnit tests inside the web container.
## Usage: phpunit
## Example: "ddev phpunit" or with additional arguments such as "ddev phpunit --testdox"
```
These will be parsed and shown when someone runs `ddev phpunit -h`, and can be used to show various examples such as adding additional arguments for the PHPUnit command.
With this all in place, we can run commands like `ddev phpunit` or `ddev phpunit --testdox`, or even `ddev phpunit modules/custom/opdavies_talks --filter=TalkEventDateTest` for a Drupal project, and have that command and tests running inside DDEV!
For more information on DDEV and creating custom commands, see the [DDEV documentation](https://ddev.readthedocs.io/en/stable/users/extend/custom-commands).

View file

@ -0,0 +1,366 @@
---
title: Creating a Custom PHPUnit Command for Docksal
date: 2018-05-06
excerpt:
How to write custom commands for Docksal, including one to easily run PHPUnit
tests in Drupal 8.
tags:
- docksal
- drupal
- drupal-8
- drupal-planet
- phpunit
- testing
---
This week Ive started writing some custom commands for my Drupal projects that
use Docksal, including one to easily run PHPUnit tests in Drupal 8. This is the
process of how I created this command.
## What is Docksal?
Docksal is a local Docker-based development environment for Drupal projects and
other frameworks and CMSes. It is our standard tool for local environments for
projects at [Microserve][0].
There was a [great talk][1] recently at Drupaldelphia about Docksal.
## Why write a custom command?
One of the things that Docksal offers (and is covered in the talk) is the
ability to add custom commands to the Docksals `fin` CLI, either globally or as
part of your project.
As an advocate of automated testing and TDD practitioner, I write a lot of tests
and run PHPUnit numerous times a day. Ive also given [talks][6] and have
[written other posts][7] on this site relating to testing in Drupal.
There are a couple of ways to run PHPUnit with Docksal. The first is to use
`fin bash` to open a shell into the container, move into the docroot directory
if needed, and run the `phpunit` command.
```bash
fin bash
cd /var/www/docroot
../vendor/bin/phpunit -c core modules/custom
```
Alternatively, it can be run from the host machine using `fin exec`.
```
cd docroot
fin exec '../vendor/bin/phpunit -c core modules/custom'
```
Both of these options require multiple steps as we need to be in the `docroot`
directory where the Drupal code is located before the command can be run, and
both have quite long commands to run PHPUnit itself - some of which is repeated
every time.
By adding a custom command, I intend to:
1. Make it easier to get set up to run PHPUnit tests - i.e. setting up a
`phpunit.xml` file.
1. Make it easier to run the tests that wed written by shortening the command
and making it so it can be run anywhere within our project.
I also hoped to make it project agnostic so that I could add it onto any project
and immediately run it.
## Creating the command
Each command is a file located within the `.docksal/commands` directory. The
filename is the name of the command (e.g. `phpunit`) with no file extension.
To create the file, run this from the same directory where your `.docksal`
directory is:
```bash
mkdir -p .docksal/commands
touch .docksal/commands/phpunit
```
This will create a new, empty `.docksal/commands/phpunit` file, and now the
`phpunit` command is now listed under "Custom commands" when we run `fin`.
![](/images/blog/docksal-phpunit-command/1.gif)
You can write commands with any interpreter. Im going to use bash, so Ill add
the shebang to the top of the file.
```bash
#!/usr/bin/env bash
```
With this in place, I can now run `fin phpunit`, though there is no output
displayed or actions performed as the rest of the file is empty.
## Adding a description and help text
Currently the description for our command when we run `fin` is the default "No
description" text. Id like to add something more relevant, so Ill start by
adding a new description.
fin interprets lines starting with `##` as documentation - the first of which it
uses as the description.
```bash
#!/usr/bin/env bash
## Run automated PHPUnit tests.
```
Now when I run it, I see the new description.
![](/images/blog/docksal-phpunit-command/2.gif)
Any additional lines are used as help text with running `fin help phpunit`. Here
Ill add an example command to demonstrate how to run it as well as some more
in-depth text about what the command will do.
```bash
#!/usr/bin/env bash
## Run automated PHPUnit tests.
##
## Usage: fin phpunit <args>
##
## If a core/phpunit.xml file does not exist, copy one from elsewhere.
## Then run the tests.
```
Now when I run `fin help phpunit`, I see the new help text.
![](/images/blog/docksal-phpunit-command/3.gif)
## Adding some content
### Setting the target
As I want the commands to be run within Docksals "cli" container, I can specify
that with `exec_target`. If one isnt specified, the commands are run locally on
the host machine.
```
#: exec_target = cli
```
### Available variables
These variables are provided by fin and are available to use within any custom
commands:
- `PROJECT_ROOT` - The absolute path to the nearest `.docksal` directory.
- `DOCROOT` - name of the docroot folder.
- `VIRTUAL_HOST` - the virtual host name for the project. Such as
`myproject.docksal`.
- `DOCKER_RUNNING` - (string) "true" or "false".
<div class="note" markdown="1">
**Note:** If the `DOCROOT` variable is not defined within the cli container, ensure that its added to the environment variables in `.docksal/docksal.yml`. For example:
```
version: "2.1"
services:
cli:
environment:
- DOCROOT
```
</div>
### Running phpunit
When you run the `phpunit` command, there are number of options you can pass to
it such as `--filter`, `--testsuite` and `--group`, as well as the path to the
tests to execute, such as `modules/custom`.
I wanted to still be able to do this by running `fin phpunit <args>` so the
commands can be customised when executed. However, as the first half of the
command (`../vendor/bin/phpunit -c core`) is consistent, I can wrap that within
my custom command and not need to type it every time.
By using `"$@"` I can capture any additional arguments, such as the test
directory path, and append them to the command to execute.
Im using `$PROJECT_ROOT` to prefix the command with the absolute path to
`phpunit` so that I dont need to be in that directory when I run the custom
command, and `$DOCROOT` to always enter the sub-directory where Drupal is
located. In this case, its "docroot" though I also use "web" and Ive seen
various others used.
```bash
DOCROOT_PATH="${PROJECT_ROOT}/${DOCROOT}"
DRUPAL_CORE_PATH="${DOCROOT_PATH}/core"
# If there is no phpunit.xml file, copy one from elsewhere.
# Otherwise run the tests.
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
```
For example, `fin phpunit modules/custom` would execute
`/var/www/vendor/bin/phpunit -c /var/www/docroot/core modules/custom` within the
container.
I can then wrap this within a condition so that the tests are only run when a
`phpunit.xml` file exists, as it is required for them to run successfully.
```bash
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
# If there is no phpunit.xml file, copy one from elsewhere.
else
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
fi
```
### Creating phpunit.xml - step 1
My first thought was that if a `phpunit.xml` file doesnt exist was to duplicate
cores `phpunit.xml.dist` file. However this isnt enough to run the tests, as
values such as `SIMPLETEST_BASE_URL`, `SIMPLETEST_DB` and
`BROWSERTEST_OUTPUT_DIRECTORY` need to be populated.
As the tests wouldn't run at this point, Ive exited early and displayed a
message to the user to edit the new `phpunit.xml` file and run `fin phpunit`
again.
```bash
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
echo "Copying ${DRUPAL_CORE_PATH}/phpunit.xml.dist to ${DRUPAL_CORE_PATH}/phpunit.xml."
echo "Please edit it's values as needed and re-run 'fin phpunit'."
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
exit 1;
else
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
fi
```
However this isnt as streamlined as I originally wanted as it still requires
the user to perform an additional step before the tests can run.
### Creating phpunit.xml - step 2
My second idea was to keep a pre-configured file within the project repository,
and to copy that into the expected location. That approach would mean that the
project specific values would already be populated, as well as any
customisations made to the default settings. I decided on
`.docksal/drupal/core/phpunit.xml` to be the potential location.
Also, if this file is copied then we can go ahead and run the tests straight
away rather than needing to exit early.
If a pre-configured file doesnt exist, then we can default back to copying
`phpunit.xml.dist`.
To avoid duplication, I created a reusable `run_tests()` function so it could be
executed in either scenario.
```bash
run_tests() {
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
}
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
if [ -e "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ]; then
echo "Copying ${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml to ${DRUPAL_CORE_PATH}/phpunit.xml"
cp "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ${DRUPAL_CORE_PATH}/phpunit.xml
run_tests "$@"
else
echo "Copying ${DRUPAL_CORE_PATH}/phpunit.xml.dist to ${DRUPAL_CORE_PATH}/phpunit.xml."
echo "Please edit it's values as needed and re-run 'fin phpunit'."
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
exit 1;
fi
else
run_tests "$@"
fi
```
This means that I can execute less steps and run a much shorter command compared
to the original, and even if someone didnt have a `phpunit.xml` file created
they could have copied into place and have tests running with only one command.
## The finished file
```bash
#!/usr/bin/env bash
#: exec_target = cli
## Run automated PHPUnit tests.
##
## Usage: fin phpunit <args>
##
## If a core/phpunit.xml file does not exist, one is copied from
## .docksal/core/phpunit.xml if that file exists, or copied from the default
## core/phpunit.xml.dist file.
DOCROOT_PATH="${PROJECT_ROOT}/${DOCROOT}"
DRUPAL_CORE_PATH="${DOCROOT_PATH}/core"
run_tests() {
${PROJECT_ROOT}/vendor/bin/phpunit -c ${DRUPAL_CORE_PATH} "$@"
}
if [ ! -e ${DRUPAL_CORE_PATH}/phpunit.xml ]; then
if [ -e "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ]; then
echo "Copying ${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml to ${DRUPAL_CORE_PATH}/phpunit.xml"
cp "${PROJECT_ROOT}/.docksal/drupal/core/phpunit.xml" ${DRUPAL_CORE_PATH}/phpunit.xml
run_tests "$@"
else
echo "Copying phpunit.xml.dist to phpunit.xml"
echo "Please edit it's values as needed and re-run 'fin phpunit'."
cp ${DRUPAL_CORE_PATH}/phpunit.xml.dist ${DRUPAL_CORE_PATH}/phpunit.xml
exit 0;
fi
else
run_tests "$@"
fi
```
Its currently available as a [GitHub Gist][2], though Im planning on moving it
into a public GitHub repository either on my personal account or the [Microserve
organisation][3], for people to either use as examples or to download and use
directly.
Ive also started to add other commands to projects such as `config-export` to
standardise the way to export configuration from Drupal 8, run Drupal 7 tests
with SimpleTest, and compile front-end assets like CSS within custom themes.
I think its a great way to shorten existing commands, or to group multiple
commands into one like in this case, and I can see a lot of other potential uses
for it during local development and continuous integration. Also being able to
run one command like `fin init` and have it set up everything for your project
is very convenient and a big time saver!
<div class="note" markdown="1">
Since writing this post, Ive had a [pull request][8] accepted for this command to be added as a [Docksal add-on][9]. This means that the command can be added to any Docksal project by running `fin addon install phpunit`. It will be installed into the `.docksal/addons/phpunit` directory, and displayed under "Addons" rather than "Custom commands" when you run `fin`.
</div>
## Resources
- [PHPUnit](https://phpunit.de)
- [PHPUnit in Drupal 8][4]
- [Main Docksal website](https://docksal.io)
- [Docksal documentation](https://docksal.readthedocs.io)
- [Docksal: one tool to rule local and CI/CD environments][1] - Docksal talk
from Drupaldelphia
- [phpcs example custom command][5]
- [phpunit command Gist][2]
- [Docksal addons blog post][9]
- [Docksal addons repository][10]
[0]: {{site.companies.microserve.url}}
[1]: https://youtu.be/1sjsvnx1P7g
[2]: https://gist.github.com/opdavies/72611f198ffd2da13f363ea65264b2a5
[3]: {{site.companies.microserve.github}}
[4]: https://www.drupal.org/docs/8/phpunit
[5]:
https://github.com/docksal/docksal/blob/develop/examples/.docksal/commands/phpcs
[6]: /talks/tdd-test-driven-drupal
[7]: /articles/tags/testing
[8]: https://github.com/docksal/addons/pull/15
[9]: https://blog.docksal.io/installing-addons-in-a-docksal-project-172a6c2d8a5b
[10]: https://github.com/docksal/addons

Some files were not shown because too many files have changed in this diff Show more