Re-add old posts
This commit is contained in:
parent
1e0a05e330
commit
0401e039de
19
app/SculpinKernel.php
Normal file
19
app/SculpinKernel.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Opdavies\Sculpin\Bundle\GistEmbedBundle\SculpinGistEmbedBundle;
|
||||||
|
use Sculpin\Bundle\SculpinBundle\HttpKernel\AbstractKernel;
|
||||||
|
|
||||||
|
final class SculpinKernel extends AbstractKernel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getAdditionalSculpinBundles(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
SculpinGistEmbedBundle::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ sculpin_content_types:
|
||||||
pages:
|
pages:
|
||||||
permalink: /:basename/
|
permalink: /:basename/
|
||||||
posts:
|
posts:
|
||||||
enabled: false
|
permalink: blog/:basename/
|
||||||
|
taxonomies: [tags]
|
||||||
talks:
|
talks:
|
||||||
permalink: talks/:basename/
|
permalink: talks/:basename/
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
"opdavies/sculpin-gist-embed-bundle": "^0.1.0",
|
||||||
"sculpin/sculpin": "^3.0"
|
"sculpin/sculpin": "^3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -17,5 +18,8 @@
|
||||||
"generate": "sculpin generate --clean --no-interaction",
|
"generate": "sculpin generate --clean --no-interaction",
|
||||||
"prod": "composer run-script generate -- --env prod",
|
"prod": "composer run-script generate -- --env prod",
|
||||||
"watch": "composer run-script --timeout=0 generate -- --server --watch"
|
"watch": "composer run-script --timeout=0 generate -- --server --watch"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
composer.lock
generated
49
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "31df69ba1277f0fbdb5e4773b6180c94",
|
"content-hash": "7f7c39b49ce875663ec830ec506fc764",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "dflydev/ant-path-matcher",
|
"name": "dflydev/ant-path-matcher",
|
||||||
|
@ -615,6 +615,53 @@
|
||||||
},
|
},
|
||||||
"time": "2020-01-08T21:13:37+00:00"
|
"time": "2020-01-08T21:13:37+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "opdavies/sculpin-gist-embed-bundle",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/opdavies/sculpin-gist-embed-bundle.git",
|
||||||
|
"reference": "1e7246d52638a7c80ace2272d93d9cffd6fcc3b8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/opdavies/sculpin-gist-embed-bundle/zipball/1e7246d52638a7c80ace2272d93d9cffd6fcc3b8",
|
||||||
|
"reference": "1e7246d52638a7c80ace2272d93d9cffd6fcc3b8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.3.3"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"sculpin/sculpin": "@stable"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Opdavies\\Sculpin\\Bundle\\GistEmbedBundle\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Oliver Davies",
|
||||||
|
"email": "oliver@oliverdavies.uk",
|
||||||
|
"homepage": "https://www.oliverdavies.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Allows for embedding GitHub Gists into a Sculpin site.",
|
||||||
|
"keywords": [
|
||||||
|
"sculpin"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/opdavies/sculpin-gist-embed-bundle/issues",
|
||||||
|
"source": "https://github.com/opdavies/sculpin-gist-embed-bundle/tree/master"
|
||||||
|
},
|
||||||
|
"time": "2016-04-05T19:57:22+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/container",
|
"name": "psr/container",
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
|
7
source/_layouts/post.html.twig
Normal file
7
source/_layouts/post.html.twig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'page' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{{ parent() }}
|
||||||
|
|
||||||
|
{% include 'about-author' %}
|
||||||
|
{% endblock %}
|
10
source/_pages/blog.html.twig
Normal file
10
source/_pages/blog.html.twig
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: Blog
|
||||||
|
use:
|
||||||
|
- posts
|
||||||
|
---
|
||||||
|
|
||||||
|
{% for post in data.posts %}
|
||||||
|
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
8
source/_partials/figure.html.twig
Normal file
8
source/_partials/figure.html.twig
Normal 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 italic text-sm text-center text-gray-800">
|
||||||
|
{{ caption }}
|
||||||
|
</figcaption>
|
||||||
|
{% endif %}
|
||||||
|
</figure>
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-2">Slides</h2>
|
<h2 class="mb-2">Slides</h2>
|
||||||
|
|
||||||
{% include 'talk/speakerdeck' with {
|
{% include 'speakerdeck' with {
|
||||||
data: speakerdeck,
|
data: speakerdeck,
|
||||||
} only %}
|
} only %}
|
||||||
</div>
|
</div>
|
||||||
|
|
10
source/_partials/tweet.html.twig
Normal file
10
source/_partials/tweet.html.twig
Normal 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>
|
9
source/_partials/video-embed.html.twig
Normal file
9
source/_partials/video-embed.html.twig
Normal 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>
|
84
source/_posts/2014.md
Normal file
84
source/_posts/2014.md
Normal 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&src=hash">#dclondon</a> very well prepared and presented. <a href="http://t.co/tDINp2Nsbn">pic.twitter.com/tDINp2Nsbn</a></p>— 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,
|
30
source/_posts/accessible-bristol-site.md
Normal file
30
source/_posts/accessible-bristol-site.md
Normal 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.
|
44
source/_posts/add-date-popup-calendar-custom-form.md
Normal file
44
source/_posts/add-date-popup-calendar-custom-form.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
title: How to add a date popup calendar onto a custom form
|
||||||
|
date: 2012-05-23
|
||||||
|
excerpt: How to use a date popup calendar within your custom module.
|
||||||
|
tags:
|
||||||
|
- forms
|
||||||
|
- form-api
|
||||||
|
- date
|
||||||
|
- calendar
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
- drupal
|
||||||
|
---
|
||||||
|
|
||||||
|
How to use a date popup calendar within your custom module.
|
||||||
|
|
||||||
|
First, I need to download the
|
||||||
|
[Date](http://drupal.org/project/date 'Date module on Drupal.org') module, and
|
||||||
|
make my module dependent on date_popup by adding the following line into my
|
||||||
|
module's .info file.
|
||||||
|
|
||||||
|
```language-ini
|
||||||
|
dependencies[] = date_popup
|
||||||
|
```
|
||||||
|
|
||||||
|
Within my form builder function:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$form['date'] = array(
|
||||||
|
'#title' => t('Arrival date'),
|
||||||
|
|
||||||
|
// Provided by the date_popup module
|
||||||
|
'#type' => 'date_popup',
|
||||||
|
|
||||||
|
// Uses the PHP date() format - http://php.net/manual/en/function.date.php
|
||||||
|
'#date_format' => 'j F Y',
|
||||||
|
|
||||||
|
// Limits the year range to the next two upcoming years
|
||||||
|
'#date_year_range' => '0:+2',
|
||||||
|
|
||||||
|
// Default value must be in 'Y-m-d' format.
|
||||||
|
'#default_value' => date('Y-m-d', time()),
|
||||||
|
);
|
||||||
|
```
|
79
source/_posts/add-taxonomy-term-multiple-nodes-using-sql.md
Normal file
79
source/_posts/add-taxonomy-term-multiple-nodes-using-sql.md
Normal 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:
|
||||||
|
- taxonomy
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- sql
|
||||||
|
- sequal-pro
|
||||||
|
- database
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
84
source/_posts/adding-custom-theme-templates-drupal-7.md
Normal file
84
source/_posts/adding-custom-theme-templates-drupal-7.md
Normal 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-planet
|
||||||
|
- drupal
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
108
source/_posts/announcing-the-drupal-vm-generator.md
Normal file
108
source/_posts/announcing-the-drupal-vm-generator.md
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
---
|
||||||
|
title: Announcing the Drupal VM Generator
|
||||||
|
date: 2016-02-15
|
||||||
|
excerpt:
|
||||||
|
For the past few weeks, I’ve 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, I’ve been working on a personal side project based on
|
||||||
|
Drupal VM. It’s called the [Drupal VM Generator][1], and over the weekend I’ve
|
||||||
|
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]. It’s 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, I’d make a
|
||||||
|
copy of `example.config.yml`, rename it to `config.yml` and edit it as needed,
|
||||||
|
but a lot of the examples aren’t needed for every use case. If you’re using
|
||||||
|
Nginx as your webserver, then you don’t need the Apache virtual hosts. If you
|
||||||
|
are not using Solr on this project, then you don’t need the Solr variables.
|
||||||
|
|
||||||
|
For a few months, I’ve 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.
|
||||||
|
|
||||||
|
Here’s 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. I’m 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
|
||||||
|
I’m more than happy to receive pull requests.
|
||||||
|
|
||||||
|
If you’re 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
|
192
source/_posts/automating-sculpin-jenkins.md
Normal file
192
source/_posts/automating-sculpin-jenkins.md
Normal 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:
|
||||||
|
- sculpin
|
||||||
|
- jenkins
|
||||||
|
---
|
||||||
|
|
||||||
|
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 don’t 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 don’t need any write access back to the repo, using the HTTP URL rather
|
||||||
|
than the SSH one was fine, and I didn’t need to provide any additional
|
||||||
|
credentials.
|
||||||
|
|
||||||
|
Also, as I knew that I’d 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 didn’t 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 I’ll end up creating a
|
||||||
|
separate `sculpin_build` item that’s decoupled from the site that it’s 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 I’d 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.
|
73
source/_posts/back-to-the-future-git-diff-apply.md
Normal file
73
source/_posts/back-to-the-future-git-diff-apply.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
title: Back to the future with Git’s 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 “there’s 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 couldn’t
|
||||||
|
push to the remote (force pushing, quite rightly, isn’t allowed for the
|
||||||
|
production branch), and using `git revert` was resulting in merge conflicts in
|
||||||
|
`composer.lock` that I’d 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,
|
||||||
|
I’m 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 there’s only one command to run and no leftover patch file, and
|
||||||
|
I can go ahead and add and commit the changes straight away.
|
102
source/_posts/building-gmail-filters-in-php.md
Normal file
102
source/_posts/building-gmail-filters-in-php.md
Normal 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:
|
||||||
|
- php
|
||||||
|
- gmail
|
||||||
|
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) I’m not that familiar with Ruby so the syntax wasn’t that
|
||||||
|
natural to me - it’s 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.
|
||||||
|
|
||||||
|
It’s 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
|
37
source/_posts/building-the-new-phpsw-website.md
Normal file
37
source/_posts/building-the-new-phpsw-website.md
Normal 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>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/968224364129906688?ref_src=twsrc%5Etfw">February 26, 2018</a></blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
It’s built with Symfony so it’s naturally using Twig for templating. I’ve become
|
||||||
|
a big fan of the utility based approach to CSS and [Tailwind CSS][1] in
|
||||||
|
particular, so I’m 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 we’re 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.
|
||||||
|
|
||||||
|
We’re 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
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
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:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- drupal
|
||||||
|
- sql
|
||||||
|
- sequel-pro
|
||||||
|
- database
|
||||||
|
- content-types
|
||||||
|
---
|
||||||
|
|
||||||
|
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'.
|
68
source/_posts/checking-if-user-logged-drupal-right-way.md
Normal file
68
source/_posts/checking-if-user-logged-drupal-right-way.md
Normal 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.
|
22
source/_posts/checkout-specific-revision-svn-command-line.md
Normal file
22
source/_posts/checkout-specific-revision-svn-command-line.md
Normal 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
|
||||||
|
```
|
29
source/_posts/conditional-email-addresses-webform.md
Normal file
29
source/_posts/conditional-email-addresses-webform.md
Normal 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:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- conditional-email
|
||||||
|
- 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.
|
70
source/_posts/configuring-the-reroute-email-module.md
Normal file
70
source/_posts/configuring-the-reroute-email-module.md
Normal 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.
|
50
source/_posts/create-and-apply-patches.md
Normal file
50
source/_posts/create-and-apply-patches.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
title: How to Create and Apply Patches
|
||||||
|
date: 2010-10-10
|
||||||
|
excerpt: How to create and apply patches, ready for the Drupal.org issue queues.
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- modules
|
||||||
|
- patches
|
||||||
|
---
|
||||||
|
|
||||||
|
Earlier this year, I posted a solution to
|
||||||
|
[an issue](http://drupal.org/node/753898) on the Drupal.org issue queue.
|
||||||
|
Originally, I just posted the code back onto the issue, but have now created a
|
||||||
|
patch that can easily be applied to any Drupal 6 installation. Here is a
|
||||||
|
run-through of the process of creating and applying a patch. In this case, I
|
||||||
|
made changes to the `user_pass_validate()` function that's found within
|
||||||
|
`modules/user/user.pages.inc`.
|
||||||
|
|
||||||
|
To begin with, a download a fresh copy of Drupal 6.19 and created a copy of the
|
||||||
|
original user.pages.inc file. Within the duplicate file, I made the same changes
|
||||||
|
to the function that I did in earlier code, and saved the changes. Now, within
|
||||||
|
my Terminal, I can navigate to Drupal's root directory and create the patch.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
diff -rup modules/user/user.pages.inc modules/user/user.pages2.inc > /Users/oliver/Desktop/different_messages_for_blocked_users.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
This command compares the differences between the two files, and creates the
|
||||||
|
specified patch file.
|
||||||
|
|
||||||
|
To apply the patch to my Drupal installation, I go back to Terminal and run the
|
||||||
|
following code:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
patch -p0 < /Users/oliver/Desktop/different_messages_for_blocked_users.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
If, for some reason, I need to reverse the patch, I can run this code:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
patch -p0 -R < /Users/oliver/Desktop/different_messages_for_blocked_users.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it!
|
||||||
|
|
||||||
|
There is also a Git patch creation workflow, which is described at
|
||||||
|
<http://groups.drupal.org/node/91424>. Thanks to
|
||||||
|
[Randy Fay](http://randyfay.com) for making me aware of this, and suggesting a
|
||||||
|
slight change to my original patch creation command.
|
161
source/_posts/create-better-photo-gallery-drupal-part-1.md
Normal file
161
source/_posts/create-better-photo-gallery-drupal-part-1.md
Normal 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:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- photo-gallery
|
||||||
|
- sql
|
||||||
|
- views
|
||||||
|
- sequel-pro
|
||||||
|
- cck
|
||||||
|
- views-attach
|
||||||
|
- drupal
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
58
source/_posts/create-better-photo-gallery-drupal-part-2.md
Normal file
58
source/_posts/create-better-photo-gallery-drupal-part-2.md
Normal 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-planet
|
||||||
|
- drupal-6
|
||||||
|
- photo-gallery
|
||||||
|
- sql
|
||||||
|
- sequel-pro
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
64
source/_posts/create-better-photo-gallery-drupal-part-21.md
Normal file
64
source/_posts/create-better-photo-gallery-drupal-part-21.md
Normal 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.
|
49
source/_posts/create-better-photo-gallery-drupal-part-3.md
Normal file
49
source/_posts/create-better-photo-gallery-drupal-part-3.md
Normal 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']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
```
|
|
@ -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-planet
|
||||||
|
- drupal-6
|
||||||
|
- drupal
|
||||||
|
- views
|
||||||
|
- nodequeue
|
||||||
|
- oliverdavies.co.uk
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
|
@ -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.
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
title: Create Multigroups in Drupal 7 using Field Collections
|
||||||
|
date: 2011-08-28
|
||||||
|
excerpt:
|
||||||
|
How to replicate CCK’s multigroups in Drupal 7 using the Field Collections
|
||||||
|
module.
|
||||||
|
tags:
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
- cck
|
||||||
|
- fields
|
||||||
|
- field-collection
|
||||||
|
- entity-api
|
||||||
|
- 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.
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
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-7
|
||||||
|
- omega
|
||||||
|
- theming
|
||||||
|
- less
|
||||||
|
- drupal-planet
|
||||||
|
- drupal
|
||||||
|
---
|
||||||
|
|
||||||
|
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_.
|
|
@ -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 Drupal’s Fancy Slide module.
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal
|
||||||
|
- drupal-6
|
||||||
|
- 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.
|
|
@ -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-planet
|
||||||
|
- drupal-6
|
||||||
|
- 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.
|
40
source/_posts/create-zen-sub-theme-using-drush.md
Normal file
40
source/_posts/create-zen-sub-theme-using-drush.md
Normal 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.
|
162
source/_posts/creating-and-using-custom-tokens-drupal-7.md
Normal file
162
source/_posts/creating-and-using-custom-tokens-drupal-7.md
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
---
|
||||||
|
title: Creating and using custom tokens in Drupal 7
|
||||||
|
date: 2013-02-16
|
||||||
|
excerpt:
|
||||||
|
This post outlines the steps required to create your own custom tokens in
|
||||||
|
Drupal.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-7
|
||||||
|
- tokens
|
||||||
|
---
|
||||||
|
|
||||||
|
This post outlines the steps required to create your own custom tokens in
|
||||||
|
Drupal.
|
||||||
|
|
||||||
|
When writing the recent releases of the
|
||||||
|
[Copyright Block](http://drupal.org/project/copyright_block) module, I used
|
||||||
|
tokens to allow the user to edit and customise their copyright message and place
|
||||||
|
the copyright_message:dates token in the desired position. When the block is
|
||||||
|
rendered, the token is replaced by the necessary dates.
|
||||||
|
|
||||||
|
We will be using the fictional _foo_ module to demonstrate this.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Token module](http://drupal.org/project/token)
|
||||||
|
|
||||||
|
## Recommended
|
||||||
|
|
||||||
|
- [Devel module](http://drupal.org/project/devel) - useful to run `dpm()` and
|
||||||
|
`kpr()` functions
|
||||||
|
- [Copyright Block module](http://drupal.org/project/copyright_block) - 7.x-2.x
|
||||||
|
and 6.x-1.x use tokens, handy as a reference
|
||||||
|
|
||||||
|
## Implementing hook_token_info()
|
||||||
|
|
||||||
|
The first thing that we need to do is define the new token type and/or the token
|
||||||
|
itself, along with it's descriptive text. To view the existing tokens and types,
|
||||||
|
use `dpm(token_get_info());`, assuming that you have the
|
||||||
|
[Devel module](http://drupal.org/project/devel) installed.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
/**
|
||||||
|
* Implements hook_token_info().
|
||||||
|
*/
|
||||||
|
function foo_token_info() {
|
||||||
|
$info = array();
|
||||||
|
|
||||||
|
// Add any new tokens.
|
||||||
|
$info['tokens']['foo']['bar'] = t('This is my new bar token within the foo type.');
|
||||||
|
|
||||||
|
// Return them.
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, the token called _bar_ resides within the _foo_ group.
|
||||||
|
|
||||||
|
If I needed to add a new token within an existing token type, such as 'node',
|
||||||
|
the syntax would be `$info['tokens']['node']['bar']`.
|
||||||
|
|
||||||
|
## Implementing hook_tokens()
|
||||||
|
|
||||||
|
Now that the Token module is aware of our new token, we now need to determine
|
||||||
|
what the token is replaced with. This is done using `hook_tokens()`. Here is the
|
||||||
|
basic code needed for an implementation:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
/**
|
||||||
|
* Implements hook_tokens().
|
||||||
|
*/
|
||||||
|
function foo_tokens($type, $tokens, array $data = array(), array $options = array()) {
|
||||||
|
$replacements = array();
|
||||||
|
|
||||||
|
// Code goes here...
|
||||||
|
|
||||||
|
// Return the replacements.
|
||||||
|
return $replacements;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first thing to check for is the type of token using an `if()` function, as
|
||||||
|
this could be an existing type like 'node', 'user' or 'site', or a custom token
|
||||||
|
type like 'foo'. Once we're sure that we're looking at the right type(s), we can
|
||||||
|
use `foreach ($tokens as $name => $original)` to loop through each of the
|
||||||
|
available tokens using a `switch()`. For each token, you can perform some logic
|
||||||
|
to work out the replacement text and then add it into the replacements array
|
||||||
|
using `$replacements[$original] = $new;`.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
/**
|
||||||
|
* Implements hook_tokens().
|
||||||
|
*/
|
||||||
|
function foo_tokens($type, $tokens, array $data = array(), array $options = array()) {
|
||||||
|
$replacements = array();
|
||||||
|
|
||||||
|
// The first thing that we're going to check for is the type of token - node,
|
||||||
|
// user etc...
|
||||||
|
if ($type == 'foo') {
|
||||||
|
// Loop through each of the available tokens.
|
||||||
|
foreach ($tokens as $name => $original) {
|
||||||
|
// Find the desired token by name
|
||||||
|
switch ($name) {
|
||||||
|
case 'bar':
|
||||||
|
$new = '';
|
||||||
|
|
||||||
|
// Work out the value of $new...
|
||||||
|
|
||||||
|
// Add the new value into the replacements array.
|
||||||
|
$replacements[$original] = $new;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the replacements.
|
||||||
|
return $replacements;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
An example from Copyright Block module:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
/**
|
||||||
|
* Implements hook_tokens().
|
||||||
|
*/
|
||||||
|
function copyright_block_tokens($type, $tokens, array $data = array(), array $options = array()) {
|
||||||
|
$replacements = array();
|
||||||
|
|
||||||
|
if ($type == 'copyright_statement') {
|
||||||
|
foreach ($tokens as $name => $original) {
|
||||||
|
switch ($name) {
|
||||||
|
case 'dates':
|
||||||
|
$start_year = variable_get('copyright_block_start_year', date('Y'));
|
||||||
|
$current_year = date('Y');
|
||||||
|
|
||||||
|
$replacements[$original] = $start_year < $current_year ? $start_year . '-' . $current_year : $start_year;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $replacements;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using token_replace()
|
||||||
|
|
||||||
|
With everything defined, all that we now need to do is pass some text through
|
||||||
|
the `token_replace()` function to replace it with the values defined within
|
||||||
|
`hook_token()`.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$a = t('Something');
|
||||||
|
// This would use any token type - node, user etc.
|
||||||
|
$b = token_replace($a);
|
||||||
|
// This would only use foo tokens.
|
||||||
|
$c = token_replace($a, array('foo'));
|
||||||
|
```
|
366
source/_posts/creating-custom-docksal-commands.md
Normal file
366
source/_posts/creating-custom-docksal-commands.md
Normal 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 I’ve 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 Docksal’s `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. I’ve 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 we’d 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. I’m going to use bash, so I’ll 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. I’d like to add something more relevant, so I’ll 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
|
||||||
|
I’ll 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 Docksal’s "cli" container, I can specify
|
||||||
|
that with `exec_target`. If one isn’t 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 it’s 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.
|
||||||
|
|
||||||
|
I’m using `$PROJECT_ROOT` to prefix the command with the absolute path to
|
||||||
|
`phpunit` so that I don’t 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, it’s "docroot" though I also use "web" and I’ve 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 doesn’t exist was to duplicate
|
||||||
|
core’s `phpunit.xml.dist` file. However this isn’t 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, I’ve 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 isn’t 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 doesn’t 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 didn’t 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
|
||||||
|
```
|
||||||
|
|
||||||
|
It’s currently available as a [GitHub Gist][2], though I’m 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.
|
||||||
|
|
||||||
|
I’ve 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 it’s 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, I’ve 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
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
title: Creating Local and Staging sites with Drupal's Domain Module Enabled
|
||||||
|
date: 2013-07-17
|
||||||
|
excerpt: How to use aliases within Domain module for pre-production sites.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- databases
|
||||||
|
- domain
|
||||||
|
- table-prefixing
|
||||||
|
---
|
||||||
|
|
||||||
|
The
|
||||||
|
[Domain Access project](https://drupal.org/project/domain 'The Domain Access project on Drupal.org')
|
||||||
|
is a suite of modules that provide tools for running a group of affiliated sites
|
||||||
|
from one Drupal installation and a single shared database. The issue is that the
|
||||||
|
domains are stored within the database so these are copied across when the data
|
||||||
|
is migrated between environments, whereas the domains are obviously going to
|
||||||
|
change.
|
||||||
|
|
||||||
|
Rather than changing the domain settings within the Domain module itself, the
|
||||||
|
best solution I think is to use table prefixes and create a different domain
|
||||||
|
table per environment. With a live, staging and local domains, the tables would
|
||||||
|
be named as follows:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
live_domain
|
||||||
|
local_domain
|
||||||
|
staging_domain
|
||||||
|
```
|
||||||
|
|
||||||
|
Within each site's settings.php file, define the prefix for the domain table
|
||||||
|
within the databases array so that each site is looking at the correct table for
|
||||||
|
its environment.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$databases['default']['default'] = array(
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'database' => 'foobar',
|
||||||
|
'username' => 'foo',
|
||||||
|
'password' => 'bar',
|
||||||
|
'host' => 'localhost',
|
||||||
|
'prefix' => array(
|
||||||
|
'default' => '',
|
||||||
|
'domain' => 'local_', // This will use the local_domain table.
|
||||||
|
// Add any other prefixed tables here.
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Within each environment-specific domain table, update the subdomain column to
|
||||||
|
contain the appropriate domain names.
|
83
source/_posts/croeso-php-south-wales.md
Normal file
83
source/_posts/croeso-php-south-wales.md
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
title: Croeso PHP South Wales!
|
||||||
|
date: 2018-08-01
|
||||||
|
excerpt:
|
||||||
|
Last night was the first meetup of Cardiff’s PHP South Wales user group.
|
||||||
|
tags:
|
||||||
|
- php
|
||||||
|
- php-south-wales
|
||||||
|
- meetups
|
||||||
|
has_tweets: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Last night was the first meetup of Cardiff’s [PHP South Wales user group][0]! It
|
||||||
|
was a great first event, and it was great to meet a lot of new people as well as
|
||||||
|
catch up some familiars within the 36 (according to meetup.com) attendees -
|
||||||
|
including some [PHP South West][9] regulars.
|
||||||
|
|
||||||
|
Organised by Steve and Amy McDougall, it was held in Barclays’ [Eagle Lab][1]
|
||||||
|
which was a great space, and it was cool to be back in Brunel House having
|
||||||
|
worked in that building previously whilst at Appnovation.
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'my-6',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Pretty cool being back in the centre of Cardiff. <a href="https://t.co/kh7Oi2tPDD">pic.twitter.com/kh7Oi2tPDD</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1024377438611156992?ref_src=twsrc%5Etfw">July 31, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
## Speakers
|
||||||
|
|
||||||
|
[Rob Allen][2] was the main speaker, who gave an interesting talk and a brave
|
||||||
|
live demo on serverless PHP and OpenWhisk. I always enjoy watching Rob speak,
|
||||||
|
which I’ve done a number of times at different events, and it was great to be
|
||||||
|
able to chat for a while after the meetup too.
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'my-6',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Great to see <a href="https://twitter.com/akrabat?ref_src=twsrc%5Etfw">@akrabat</a> speaking about serverless PHP at the first <a href="https://twitter.com/phpSouthWales?ref_src=twsrc%5Etfw">@phpSouthWales</a> meetup. <a href="https://twitter.com/hashtag/php?src=hash&ref_src=twsrc%5Etfw">#php</a> <a href="https://twitter.com/hashtag/phpc?src=hash&ref_src=twsrc%5Etfw">#phpc</a> <a href="https://twitter.com/hashtag/cardiff?src=hash&ref_src=twsrc%5Etfw">#cardiff</a> <a href="https://t.co/Q9YaQ6O1fB">pic.twitter.com/Q9YaQ6O1fB</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1024359937063956484?ref_src=twsrc%5Etfw">July 31, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
We also had a couple of lightning talks, starting with [Ismael Velasco][3]
|
||||||
|
giving an introduction to progressive web applications (PWAs). I can see some
|
||||||
|
potential uses for this on my current work project, and I look forward to seeing
|
||||||
|
the full talk soon).
|
||||||
|
|
||||||
|
I gave an updated version of my [Tailwind CSS lightning talk][4], and enjoyed
|
||||||
|
being able to show some examples of new sites using Tailwind such as [Laravel
|
||||||
|
Nova][5], [Spatie][6]’s new website and PHP South Wales itself!
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'my-6',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Lightning talk time, first <a href="https://twitter.com/IsmaelVelasco?ref_src=twsrc%5Etfw">@IsmaelVelasco</a> talking about <a href="https://twitter.com/hashtag/PWA?src=hash&ref_src=twsrc%5Etfw">#PWA</a> 😎🎉 <a href="https://t.co/KrJGZlIp7V">pic.twitter.com/KrJGZlIp7V</a></p>— PHP South Wales (@phpSouthWales) <a href="https://twitter.com/phpSouthWales/status/1024377906456420352?ref_src=twsrc%5Etfw">July 31, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
It’s great to have a meetup in Cardiff again, and having thought about organsing
|
||||||
|
something myself previously, I’m glad to see someone step forward to do so. This
|
||||||
|
shows that there's still a strong PHP community in Cardiff and South Wales, and
|
||||||
|
hopefully this will be the first meetup of many. I’ll look forward to seeing the
|
||||||
|
local community grow!
|
||||||
|
|
||||||
|
Thanks again to Steve and Amy for organising, Eagle Labs for hosting, the
|
||||||
|
sponsors, and Rob and Ismael for speaking.
|
||||||
|
|
||||||
|
It would be great to see even more people at the next one. If you’re interested,
|
||||||
|
take a look at the [group’s website][0], [meetup.com group][7] and [Twitter
|
||||||
|
profile][8]. Alternatively, get in touch with myself or one of the organisers
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
**Croeso ac iechyd da PHP South Wales!**
|
||||||
|
|
||||||
|
[0]: https://www.phpsouthwales.uk
|
||||||
|
[1]: https://labs.uk.barclays/locations/cardiff-en
|
||||||
|
[2]: https://twitter.com/akrabat
|
||||||
|
[3]: https://twitter.com/IsmaelVelasco
|
||||||
|
[4]: /talks/taking-flight-with-tailwind-css
|
||||||
|
[5]: https://nova.laravel.com
|
||||||
|
[6]: https://spatie.be
|
||||||
|
[7]: https://www.meetup.com/PHP-South-Wales
|
||||||
|
[8]: https://twitter.com/phpsouthwales
|
||||||
|
[9]: https://phpsw.uk
|
|
@ -0,0 +1,144 @@
|
||||||
|
---
|
||||||
|
title:
|
||||||
|
Debugging Drupal Commerce Promotions and Adjustments using Illuminate
|
||||||
|
Collections (Drupal 8)
|
||||||
|
date: 2018-10-24
|
||||||
|
excerpt:
|
||||||
|
Using Laravel’s Illuminate Collections to debug an issue with a Drupal
|
||||||
|
Commerce promotion.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-8
|
||||||
|
- drupal-commerce
|
||||||
|
- drupal-planet
|
||||||
|
- illuminate-collections
|
||||||
|
- laravel-collections
|
||||||
|
- php
|
||||||
|
promoted: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Today I found another instance where I decided to use [Illuminate
|
||||||
|
Collections][0] within my Drupal 8 code; whilst I was debugging an issue where a
|
||||||
|
[Drupal Commerce][1] promotion was incorrectly being applied to an order.
|
||||||
|
|
||||||
|
No adjustments were showing in the Drupal UI for that order, so after some
|
||||||
|
initial investigation and finding that `$order->getAdjustments()` was empty, I
|
||||||
|
determined that I would need to get the adjustments from each order item within
|
||||||
|
the order.
|
||||||
|
|
||||||
|
If the order were an array, this is how it would be structured in this
|
||||||
|
situation:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$order = [
|
||||||
|
'id' => 1,
|
||||||
|
'items' => [
|
||||||
|
[
|
||||||
|
'id' => 1,
|
||||||
|
'adjustments' => [
|
||||||
|
['name' => 'Adjustment 1'],
|
||||||
|
['name' => 'Adjustment 2'],
|
||||||
|
['name' => 'Adjustment 3'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 2,
|
||||||
|
'adjustments' => [
|
||||||
|
['name' => 'Adjustment 4'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 3,
|
||||||
|
'adjustments' => [
|
||||||
|
['name' => 'Adjustment 5'],
|
||||||
|
['name' => 'Adjustment 6'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting the order items
|
||||||
|
|
||||||
|
I started by using `$order->getItems()` to load the order’s items, converted
|
||||||
|
them into a Collection, and used the Collection’s `pipe()` method and the
|
||||||
|
`dump()` function provided by the [Devel module][2] to output the order items.
|
||||||
|
|
||||||
|
```php
|
||||||
|
collect($order->getItems())
|
||||||
|
->pipe(function (Collection $collection) {
|
||||||
|
dump($collection);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get the order item adjustments
|
||||||
|
|
||||||
|
Now we have a Collection of order items, for each item we need to get it’s
|
||||||
|
adjustments. We can do this with `map()`, then call `getAdjustments()` on the
|
||||||
|
order item.
|
||||||
|
|
||||||
|
This would return a Collection of arrays, with each array containing it’s own
|
||||||
|
adjustments, so we can use `flatten()` to collapse all the adjustments into one
|
||||||
|
single-dimensional array.
|
||||||
|
|
||||||
|
```php
|
||||||
|
collect($order->getItems())
|
||||||
|
->map(function (OrderItem $order_item) {
|
||||||
|
return $order_item->getAdjustments();
|
||||||
|
})
|
||||||
|
->flatten(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a couple of refactors that we can do here though:
|
||||||
|
|
||||||
|
- Use `flatMap()` to combine the `flatten()` and `map()` methods.
|
||||||
|
- Use [higher order messages][3] to delegate straight to the `getAdjustments()`
|
||||||
|
method on the order, rather than having to create a closure and call the
|
||||||
|
method within it.
|
||||||
|
|
||||||
|
```php
|
||||||
|
collect($order->getItems())
|
||||||
|
->flatMap->getAdjustments();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
In this scenario, each order item had three adjustments - the correct promotion,
|
||||||
|
the incorrect one and the standard VAT addition. I wasn’t concerned about the
|
||||||
|
VAT adjustment for debugging, so I used `filter()` to remove it based on the
|
||||||
|
result of the adjustment’s `getSourceId()` method.
|
||||||
|
|
||||||
|
```php
|
||||||
|
collect($order->getItems())
|
||||||
|
->flatMap->getAdjustments()
|
||||||
|
->filter(function (Adjustment $adjustment) {
|
||||||
|
return $adjustment->getSourceId() != 'vat';
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Now I have just the relevant adjustments, I want to be able to load each one to
|
||||||
|
load it and check it’s conditions. To do this, I need just the source IDs.
|
||||||
|
|
||||||
|
Again, I can use a higher order message to directly call `getSourceId()` on the
|
||||||
|
adjustment and return it’s value to `map()`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
collect($order->getItems())
|
||||||
|
->flatMap->getAdjustments()
|
||||||
|
->filter(function (Adjustment $adjustment) {
|
||||||
|
return $adjustment->getSourceId() != 'vat';
|
||||||
|
})
|
||||||
|
->map->getSourceId();
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns a Collection containing just the relevant promotion IDs being
|
||||||
|
applied to the order that I can use for debugging.
|
||||||
|
|
||||||
|
Now just to find out why the incorrect promotion was applying!
|
||||||
|
|
||||||
|
[0]: https://laravel.com/docs/collections
|
||||||
|
[1]: https://drupalcommerce.org
|
||||||
|
[2]: https://www.drupal.org/project/devel
|
||||||
|
[3]: https://laravel-news.com/higher-order-messaging
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
title: Display a Custom Menu in a Drupal 7 Theme Template File
|
||||||
|
date: 2012-08-18
|
||||||
|
excerpt: The code needed to display a menu in a Drupal 7 template file.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
- php
|
||||||
|
- aria
|
||||||
|
---
|
||||||
|
|
||||||
|
For reference, this is the code needed to display a menu in a Drupal 7 template
|
||||||
|
file, including the navigation ARIA role.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$menu_name = 'menu-footer-menu';
|
||||||
|
$menu_id = 'footer-menu';
|
||||||
|
print theme('links', array(
|
||||||
|
'links' => menu_navigation_links($menu_name),
|
||||||
|
'attributes' => array(
|
||||||
|
'id' => $menu_id,
|
||||||
|
'role' => 'navigation',
|
||||||
|
'class'=> array('links', 'inline')
|
||||||
|
)
|
||||||
|
));
|
||||||
|
```
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
title: Display Git Branch or Tag Names in your Bash Prompt
|
||||||
|
date: 2013-04-27
|
||||||
|
excerpt:
|
||||||
|
Whilst watching Drupalize.me's recent Introduction to Git series, I thought it
|
||||||
|
was useful the way that the current Git branch or tag name was displayed in
|
||||||
|
the bash prompt. Here's how to do it.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- git
|
||||||
|
- terminal
|
||||||
|
---
|
||||||
|
|
||||||
|
Whilst watching [Drupalize.me](http://drupalize.me 'Drupalize.me')'s recent
|
||||||
|
[Introduction to Git series](http://drupalize.me/series/introduction-git-series 'Introduction to Git on Drupalize.me'),
|
||||||
|
I thought it was useful the way that the current Git branch or tag name was
|
||||||
|
displayed in the bash prompt.
|
||||||
|
|
||||||
|
Here's how to do it.
|
||||||
|
|
||||||
|
For example (with some slight modifications):
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
oliver@oliver-mbp:~/Development/drupal(master) $
|
||||||
|
oliver@oliver-mbp:~/Development/a11y_checklist(7.x-1.0) $
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's how to do it.
|
||||||
|
|
||||||
|
To begin with, create a new file to contain the functions,
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
vim ~/.bash/git-prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste the following code into the file, and save it.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
parse_git_branch () {
|
||||||
|
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_git_tag () {
|
||||||
|
git describe --tags 2> /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_git_branch_or_tag() {
|
||||||
|
local OUT="$(parse_git_branch)"
|
||||||
|
if [ "$OUT" == " ((no branch))" ]; then
|
||||||
|
OUT="($(parse_git_tag))";
|
||||||
|
fi
|
||||||
|
echo $OUT
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit your `.bashrc` or `.bash_profile` file to override the PS1 value.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
vim ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following code at the bottom of the file, and save it.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
source ~/.bash/git-prompt
|
||||||
|
PS1="\u@\h:\w\$(parse_git_branch_or_tag) $ "
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart your Terminal or type `source ~/.bashrc` to see your changes.
|
30
source/_posts/display-number-facebook-fans-php.md
Normal file
30
source/_posts/display-number-facebook-fans-php.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
title: Display the Number of Facebook fans in PHP
|
||||||
|
date: 2011-03-15
|
||||||
|
excerpt: How to use PHP to display the number of fans of a Facebook page.
|
||||||
|
tags:
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace the \$page_id value with your Page ID number (unless you want to show
|
||||||
|
the number of fans for this site).You can find your Page ID by logging into your
|
||||||
|
Facebook account, going to 'Adverts and Pages', clicking 'Edit page', and
|
||||||
|
looking at the URL.
|
||||||
|
|
||||||
|
For example, mine is
|
||||||
|
<https://www.facebook.com/pages/edit/?id=143394365692197&sk=basic>.
|
||||||
|
|
||||||
|
I've also wrapped the output in a number_format() function so that it properly
|
||||||
|
formatted with commas etc - like where I've used it within the
|
||||||
|
[Gold Event listing](http://www.horseandcountry.tv/events/paid) on the Horse &
|
||||||
|
Country TV website.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$page_id = "143394365692197";
|
||||||
|
$xml = @simplexml_load_file("http://api.facebook.com/restserver.php?method=facebook.fql.query&query=SELECT%20fan_count%20FROM%20page%20WHERE%20page_id=".$page_id."") or die ("a lot");
|
||||||
|
$fans = $xml->page->fan_count;
|
||||||
|
print number_format($fans);
|
||||||
|
```
|
||||||
|
|
||||||
|
This code was originally found at
|
||||||
|
<http://wp-snippets.com/display-number-facebook-fans>.
|
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
title: Dividing Drupal's process and preprocess functions into separate files
|
||||||
|
date: 2012-05-24
|
||||||
|
excerpt:
|
||||||
|
If you use a lot of process and preprocess functions within your Drupal theme,
|
||||||
|
then your template.php can get very long and it can become difficult to find a
|
||||||
|
certain piece of code. Following the example of the Omega theme, I've started
|
||||||
|
separating my process and preprocess functions into their own files.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- code
|
||||||
|
- theming
|
||||||
|
- preprocessing
|
||||||
|
---
|
||||||
|
|
||||||
|
If you use a lot of process and preprocess functions within your Drupal theme,
|
||||||
|
then your template.php can get very long and it can become difficult to find a
|
||||||
|
certain piece of code. Following the example of the
|
||||||
|
[Omega theme](http://drupal.org/project/omega 'The Omega theme on Drupal.org'),
|
||||||
|
I've started separating my process and preprocess functions into their own
|
||||||
|
files. For example, mytheme_preprocess_node can be placed within a
|
||||||
|
preprocess/node.inc file, and mytheme_process_page can be placed within
|
||||||
|
process/page.inc.
|
||||||
|
|
||||||
|
The first step is to use the default mytheme_process() and mytheme_preprocess()
|
||||||
|
functions to utilise my custom function. So within my template.php file:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements hook_preprocess().
|
||||||
|
*
|
||||||
|
* Initialises the mytheme_invoke() function for the preprocess hook.
|
||||||
|
*/
|
||||||
|
function mytheme_preprocess(&$variables, $hook) {
|
||||||
|
mytheme_invoke('preprocess', $hook, $variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements hook_process().
|
||||||
|
*
|
||||||
|
* Initialises the mytheme_invoke() function for the process hook.
|
||||||
|
*/
|
||||||
|
function mytheme_process(&$variables, $hook) {
|
||||||
|
mytheme_invoke('process', $hook, $variables);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, to write the `mytheme_invoke()` function:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes custom process and preprocess functions.
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* The type of function we are trying to include (i.e. process or preprocess).
|
||||||
|
*
|
||||||
|
* @param array $variables
|
||||||
|
* The variables array.
|
||||||
|
*
|
||||||
|
* @param string $hook
|
||||||
|
* The name of the hook.
|
||||||
|
*
|
||||||
|
* @see mytheme_preprocess()
|
||||||
|
* @see mytheme_process()
|
||||||
|
*/
|
||||||
|
function mytheme_invoke($type, $hook, &$variables) {
|
||||||
|
global $theme_key;
|
||||||
|
|
||||||
|
// The name of the function to look for (e.g. mytheme_process_node).
|
||||||
|
$function = $theme_key . '_' . $type . '_' . $hook;
|
||||||
|
|
||||||
|
// If the function doesn't exist within template.php, look for the
|
||||||
|
// appropriate include file.
|
||||||
|
if (!function_exists($function)) {
|
||||||
|
// The file to search for (e.g. process/node.inc).
|
||||||
|
$file = drupal_get_path('theme', $theme_key) . '/' . $type . '/' . $type . '-' . str_replace('_', '-', $hook) . '.inc';
|
||||||
|
|
||||||
|
// If the file exists, include it.
|
||||||
|
if (is_file($file)) {
|
||||||
|
include($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to call the function again.
|
||||||
|
if (function_exists($function)) {
|
||||||
|
$function($variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As `mytheme_invoke()` checks to see if the function already exists before
|
||||||
|
searching for checking the include files, I could still add the functions into
|
||||||
|
template.php as normal and this would override any corresponding include file.
|
75
source/_posts/dont-bootstrap-drupal-use-drush.md
Normal file
75
source/_posts/dont-bootstrap-drupal-use-drush.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
---
|
||||||
|
title: Don't Bootstrap Drupal, Use Drush
|
||||||
|
date: 2013-11-19
|
||||||
|
excerpt:
|
||||||
|
Avoid bootstrapping Drupal manually in your scratch files - Drush has you
|
||||||
|
covered!
|
||||||
|
tags:
|
||||||
|
- drush
|
||||||
|
- drupal-planet
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
There are times when doing Drupal development when you need to run a custom PHP
|
||||||
|
script, maybe moving data from one field to another, that doesn't warrant the
|
||||||
|
time and effort to create a custom module. In this scenario, it would be quicker
|
||||||
|
to write a .php script and bootstrap Drupal to gain access to functions like
|
||||||
|
`node_load()` and `db_query()`.
|
||||||
|
|
||||||
|
To bootstrap Drupal, you would need to add some additional lines of code to the
|
||||||
|
stop of your script. Here is an alternative way.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Bootstrap Drupal.
|
||||||
|
$drupal_path = $_SERVER['DOCUMENT_ROOT'];
|
||||||
|
define('DRUPAL_ROOT', $drupal_path);
|
||||||
|
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
|
||||||
|
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
|
||||||
|
|
||||||
|
// Do stuff.
|
||||||
|
$node = node_load(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
The script would need be placed in the root of your Drupal directory, and you
|
||||||
|
would then have had to open a browser window and visit
|
||||||
|
http://example.com/foo.php to execute it. This is where the "drush php-script"
|
||||||
|
command (or "drush scr" for short) is useful, and can be used to execute the
|
||||||
|
script from the command line.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush scr foo.php
|
||||||
|
```
|
||||||
|
|
||||||
|
It also means that I no longer need to manually bootstrap Drupal, so my script
|
||||||
|
is much cleaner.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
// Just do stuff.
|
||||||
|
$node = node_load(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
I prefer to keep these scripts outside of my Drupal directory in a separate
|
||||||
|
"scripts" directory (with Drupal in a "drupal" directory on the same level).
|
||||||
|
This makes it easier to update Drupal as I don't need to worry about
|
||||||
|
accidentally deleting the additional files. From within the drupal directory, I
|
||||||
|
can now run the following command to go up one level, into the scripts directory
|
||||||
|
and then execute the script. Note that you do not need to include the file
|
||||||
|
extension.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush scr ../scripts/foo
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you're using
|
||||||
|
[Drush aliases](http://deeson-online.co.uk/labs/drupal-drush-aliases-and-how-use-them 'Drupal, Drush aliases, and how to use them'):
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush @mysite.local scr foo
|
||||||
|
```
|
||||||
|
|
||||||
|
If you commonly use the same scripts for different projects, you could also
|
||||||
|
store these within a separate Git repository and checkout the scripts directory
|
||||||
|
using a
|
||||||
|
[Git submodule](http://git-scm.com/book/en/Git-Tools-Submodules 'Git Submodules').
|
59
source/_posts/download-different-versions-drupal-drush.md
Normal file
59
source/_posts/download-different-versions-drupal-drush.md
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
title: Download Different Versions of Drupal with Drush
|
||||||
|
date: 2013-12-31
|
||||||
|
excerpt: How to download different versions of Drupal core using Drush.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- drush
|
||||||
|
---
|
||||||
|
|
||||||
|
If you use
|
||||||
|
[Drush](https://raw.github.com/drush-ops/drush/master/README.md 'About Drush'),
|
||||||
|
it's likely that you've used the `drush pm-download` (or `drush dl` for short)
|
||||||
|
command to start a new project. This command downloads projects from Drupal.org,
|
||||||
|
but if you don't specify a project or type "drush dl drupal", the command will
|
||||||
|
download the current stable version of Drupal core. Currently, this will be
|
||||||
|
Drupal 7 with that being the current stable version of core at the time of
|
||||||
|
writing this post.
|
||||||
|
|
||||||
|
But what if you don't want Drupal 7?
|
||||||
|
|
||||||
|
I still maintain a number of Drupal 6 sites and occassionally need to download
|
||||||
|
Drupal 6 core as opposed to Drupal 7. I'm also experimenting with Drupal 8 so I
|
||||||
|
need to download that as well.
|
||||||
|
|
||||||
|
By declarding the core version of Drupal, such as "drupal-6", Drush will
|
||||||
|
download that instead.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush dl drupal-6
|
||||||
|
```
|
||||||
|
|
||||||
|
This downloads the most recent stable version of Drupal 6. If you don't want
|
||||||
|
that, you can add the --select and additionally the --all options to be
|
||||||
|
presented with an entire list to chose from.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush dl drupal-6 --select
|
||||||
|
$ drush dl drupal-6 --select --all
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want the most recent development version, just type:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ drush dl drupal-6.x
|
||||||
|
```
|
||||||
|
|
||||||
|
The same can be done for other core versions of Drupal, from Drupal 5 upwards.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
# This will download Drupal 5
|
||||||
|
$ drush dl drupal-5
|
||||||
|
# This will download Drupal 8
|
||||||
|
$ drush dl drupal-8
|
||||||
|
```
|
||||||
|
|
||||||
|
For a full list of the available options, type "drush help pm-download" into a
|
||||||
|
Terminal window or take a look at the entry on
|
||||||
|
[drush.ws](http://drush.ws/#pm-download, 'The entry for pm-download on drush.ws').
|
38
source/_posts/drupal-8-5-released.md
Normal file
38
source/_posts/drupal-8-5-released.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
title: 'Drupal 8.5.0 Released'
|
||||||
|
date: 2018-03-09
|
||||||
|
excerpt: This week, the latest version of Drupal 8 was released.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-core
|
||||||
|
---
|
||||||
|
|
||||||
|
This week the latest minor version of Drupal 8, 8.5.0, was released.
|
||||||
|
|
||||||
|
> This new version makes Media module available for all, improves migrations
|
||||||
|
> significantly, stabilizes the Content Moderation and Settings Tray modules,
|
||||||
|
> serves dynamic pages faster with BigPipe enabled by default, and introduces a
|
||||||
|
> new experimental entity layout user interface. The release includes several
|
||||||
|
> very important fixes for workflows of content translations and supports
|
||||||
|
> running on PHP 7.2.
|
||||||
|
|
||||||
|
I’ve been very impressed by the new release cycle Drupal 8 and the usage of
|
||||||
|
semantic versioning. Though it adds a greater maintenance overhead for module,
|
||||||
|
theme, installation profile and distribution developers to ensure that our
|
||||||
|
projects are still working properly, having the ability to add new modules into
|
||||||
|
Drupal core as well as new installation profiles like the [Unami demonstration
|
||||||
|
profile][2] is pretty cool!
|
||||||
|
|
||||||
|
For example, in addition to Unami, 8.5 alone adds media in core, two
|
||||||
|
experimental modules have been marked as stable, an experimental new layout
|
||||||
|
builder has been added and lots of PHP 7.2 improvements have been committed to
|
||||||
|
make 8.5 fully PHP 7.2 compatible.
|
||||||
|
|
||||||
|
I’m already looking forward to see what’s coming in 8.6 later this year!
|
||||||
|
|
||||||
|
For more information on the 8.5 release, see the [blog post on Drupal.org][1].
|
||||||
|
|
||||||
|
[0]: https://dri.es/drupal-8-5-0-released
|
||||||
|
[1]: https://www.drupal.org/blog/drupal-8-5-0
|
||||||
|
[2]:
|
||||||
|
https://www.drupal.org/docs/8/umami-drupal-8-demonstration-installation-profile
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
title: "Drupal 8 Commerce: Fixing 'No Such Customer' error on checkout"
|
||||||
|
date: 2018-08-15
|
||||||
|
excerpt:
|
||||||
|
Fixing a Drupal Commerce error when a user tries to complete a checkout.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-8
|
||||||
|
- drupal-commerce
|
||||||
|
- stripe
|
||||||
|
---
|
||||||
|
|
||||||
|
Recently I was experiencing an issue on the Drupal 8 website I’m working on,
|
||||||
|
where a small number of users were not able to complete the checkout process and
|
||||||
|
instead got a generic `The site has encountered an unexpected error` message.
|
||||||
|
|
||||||
|
Looking at the log, I was able to see the error being thrown (the customer ID
|
||||||
|
has been redacted):
|
||||||
|
|
||||||
|
> Stripe\Error\InvalidRequest: No such customer: cus_xxxxxxxxxxxxxx in
|
||||||
|
> Stripe\ApiRequestor::\_specificAPIError() (line 124 of
|
||||||
|
> /var/www/vendor/stripe/stripe-php/lib/ApiRequestor.php).
|
||||||
|
|
||||||
|
Logging in to the Stripe account, I was able to confirm that the specified
|
||||||
|
customer ID did not exist. So where was it coming from, and why was Drupal
|
||||||
|
trying to retrieve a non-existent customer?
|
||||||
|
|
||||||
|
## Investigation
|
||||||
|
|
||||||
|
After some investigation, I found a table in the database named
|
||||||
|
`user__commerce_remote_id` which stores the remote customer ID for each payment
|
||||||
|
method (again, the customer ID has been redacted).
|
||||||
|
|
||||||
|
![A screenshot of a row in the user__commerce_remote_id table](/images/blog/commerce-stripe-error/remote-id-table.png){.border.p-1}
|
||||||
|
|
||||||
|
The `entity_id` and `revision_id` values in this case refer to the user that the
|
||||||
|
Stripe customer has been associated with.
|
||||||
|
|
||||||
|
As there was no customer in Stripe with this ID, I think that this must be a
|
||||||
|
customer ID from the test environment (the data from which was deleted before
|
||||||
|
the site went live).
|
||||||
|
|
||||||
|
### Drupal code
|
||||||
|
|
||||||
|
This I believe is the Drupal code where the error was being triggered:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
// modules/contrib/commerce_stripe/src/Plugin/Commerce/PaymentGateway/Stripe.php
|
||||||
|
|
||||||
|
public function createPayment(PaymentInterface $payment, $capture = TRUE) {
|
||||||
|
...
|
||||||
|
|
||||||
|
$owner = $payment_method->getOwner();
|
||||||
|
if ($owner && $owner->isAuthenticated()) {
|
||||||
|
$transaction_data['customer'] = $this->getRemoteCustomerId($owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = \Stripe\Charge::create($transaction_data);
|
||||||
|
ErrorHelper::handleErrors($result);
|
||||||
|
}
|
||||||
|
catch (\Stripe\Error\Base $e) {
|
||||||
|
ErrorHelper::handleException($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stripe code
|
||||||
|
|
||||||
|
I can also see in the Stripe library where the original error is generated.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
|
||||||
|
{
|
||||||
|
$msg = isset($errorData['message']) ? $errorData['message'] : null;
|
||||||
|
$param = isset($errorData['param']) ? $errorData['param'] : null;
|
||||||
|
$code = isset($errorData['code']) ? $errorData['code'] : null;
|
||||||
|
|
||||||
|
switch ($rcode) {
|
||||||
|
...
|
||||||
|
|
||||||
|
case 404:
|
||||||
|
return new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders);
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
After confirming that it was the correct user ID, simply removing that row from
|
||||||
|
the database allowed the new Stripe customer to be created and for the user to
|
||||||
|
check out successfully.
|
37
source/_posts/drupal-association.md
Normal file
37
source/_posts/drupal-association.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
title: Drupal Association
|
||||||
|
date: 2014-05-03
|
||||||
|
excerpt: Next week, I'll be working for the Drupal Association.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- personal
|
||||||
|
---
|
||||||
|
|
||||||
|
Today was my last day working at [Precedent](http://www.precedent.com). Next
|
||||||
|
week, I'll be starting my
|
||||||
|
[new job](https://assoc.drupal.org/node/18923 'Drupal.org Developer') at the
|
||||||
|
[Drupal Association](http://assoc.drupal.org) working on Drupal's home -
|
||||||
|
[Drupal.org](http://www.drupal.org).
|
||||||
|
|
||||||
|
I was at Precedent for just over a year and had the opportunity to work on
|
||||||
|
several Drupal projects from project leading to ad-hoc module and theme
|
||||||
|
development, including my largest Drupal build to date.
|
||||||
|
|
||||||
|
I was also lucky enough to go to
|
||||||
|
[DrupalCon Prague](http://prague2013.drupal.org) as well as
|
||||||
|
[DrupalCamp London](http://2014.drupalcamplondon.co.uk).
|
||||||
|
|
||||||
|
I was able to [contribute some code](https://drupal.org/project/eventsforce)
|
||||||
|
back into the community and encourage other team members to do the same.
|
||||||
|
|
||||||
|
It was good to be able to introduce some new tools like
|
||||||
|
[Vagrant](http://www.vagrantup.com), [Puppet](http://www.puppetlabs.com),
|
||||||
|
[SASS](http://www.sass-lang.com) and [Compass](http://www.compass-style.org)
|
||||||
|
into the team. I was pleased to introduce and champion the
|
||||||
|
[Git Flow](http://danielkummer.github.io/git-flow-cheatsheet 'Git Flow Cheat Sheet')
|
||||||
|
branching model, which them became the standard approach for all Drupal
|
||||||
|
projects, and hopefully soon all development projects.
|
||||||
|
|
||||||
|
Working for the Drupal Association and on Drupal.org was an opportunity that I
|
||||||
|
couldn't refuse, and is certainly going to be a fun and interesting challenge. I
|
||||||
|
can't wait to get started!
|
107
source/_posts/drupal-bristol-testing-workshop.md
Normal file
107
source/_posts/drupal-bristol-testing-workshop.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
---
|
||||||
|
title: Drupal Bristol Testing Workshop
|
||||||
|
date: 2018-06-28
|
||||||
|
excerpt:
|
||||||
|
Yesterday evening, I did my first workshop, held at the Drupal Bristol user
|
||||||
|
group.
|
||||||
|
tags:
|
||||||
|
[composer, docksal, drupal, drupal-8, drupal-bristol, php, phpunit, testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
Yesterday evening, I did [my first workshop][16] (and I believe, the first
|
||||||
|
workshop) held at the [{{ site.events['drupal_bristol'].name }}][14] user group.
|
||||||
|
The subject was automated testing with PHPUnit in Drupal 8, in preparation for
|
||||||
|
my talk at [{{ site.events['drupal_dev_days_18'].name }}][12] next week and to
|
||||||
|
help process some ideas for my [testing book][15].
|
||||||
|
|
||||||
|
Here are some details about what we covered, and some of my thoughts in review.
|
||||||
|
|
||||||
|
## Local Environment
|
||||||
|
|
||||||
|
Before the meetup, I set up a [repository on GitHub][0] that contains a
|
||||||
|
Composer-based Drupal 8 installation, based on the [Drupal 8 Composer
|
||||||
|
template][4] along with the [Examples module][5] (which includes some PHPUnit
|
||||||
|
tests) with a pre-configured [Docksal][2] environment to use locally - Docksal
|
||||||
|
being our standard local development environment that we use at
|
||||||
|
{{ site.companies['microserve'].name }} for all of our projects, so something
|
||||||
|
that I’m familiar with using.
|
||||||
|
|
||||||
|
In addition to the default stack, I added [the PHPUnit add-on that I wrote][6]
|
||||||
|
so that it was easier to run tests, [configured settings.php using environment
|
||||||
|
variables][7] and added a custom `fin init` command to install the Composer
|
||||||
|
dependencies and install Drupal. This meant after that installing Docksal,
|
||||||
|
everyone had a running Drupal 8 website after only running `git clone` and
|
||||||
|
`fin init`, and could then run tests straight away using
|
||||||
|
`fin phpunit web/modules/contrib/examples/phpunit_example`.
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
Once everyone was set up, we moved on to talk about why testing is important and
|
||||||
|
the different options available to run them, before looking at the different
|
||||||
|
types of tests available in Drupal 8. For each test type, I explained what it
|
||||||
|
was used for and everyone completed an exercise on each - creating a test of
|
||||||
|
that type, initially seeing it fail, and then doing the work to get it to pass.
|
||||||
|
|
||||||
|
The completed code that I wrote beforehand for these is available in their own
|
||||||
|
[GitHub repository][8], including all of the tests as well as the implementation
|
||||||
|
code.
|
||||||
|
|
||||||
|
Once these exercises were completed, we looked at creating a blog page using
|
||||||
|
test driven development - the example that I use in the [TDD - Test Driven
|
||||||
|
Drupal talk][9], to give a more real-word scenario. It would have been good to
|
||||||
|
have gone through this as an exercise too, if we’d have had more time.
|
||||||
|
|
||||||
|
## Wrap Up
|
||||||
|
|
||||||
|
To finish, I demonstrated the PHPUnit integration within PHPStorm (which is
|
||||||
|
working with Docksal) and showed some of the tests that I wrote for the [Private
|
||||||
|
Message Queue][10] and [System User][11] modules, to see how things like adding
|
||||||
|
items to queues and processing them, ensuring that emails are sent, to the right
|
||||||
|
users and contain the right data, can be tested, as well as some of the tests
|
||||||
|
that we’ve written on my work project over the last few months.
|
||||||
|
|
||||||
|
## Slides
|
||||||
|
|
||||||
|
I didn’t record this workshop, but I have exported the slides and embedded them
|
||||||
|
below:
|
||||||
|
|
||||||
|
{% include 'speakerdeck' with {
|
||||||
|
data: {
|
||||||
|
id: '2679401cb2ad421789d372cb8d38e368',
|
||||||
|
ratio: '1.77777777777778',
|
||||||
|
}
|
||||||
|
} %}
|
||||||
|
|
||||||
|
## Thoughts
|
||||||
|
|
||||||
|
I was very happy with how my first workshop went, it was a great experience for
|
||||||
|
me and it seemed that the attendees all learnt something and found it
|
||||||
|
interesting.
|
||||||
|
|
||||||
|
A couple of people mentioned about providing handouts to refer the code examples
|
||||||
|
whilst working on the exercises, rather than relying on the slides and avoiding
|
||||||
|
the need to sometimes switch back and forth between slides. I’ve found that I
|
||||||
|
can export the slide deck as PNGs or JPGs from Deckset, so I’ll definitely do
|
||||||
|
that next time.
|
||||||
|
|
||||||
|
I’m giving the [Test Driven Drupal][9] talk at the [Drupal Dev Days
|
||||||
|
conference][12] next week, and I’m hoping to give it again at other meetups and
|
||||||
|
events in the UK. If you’d like me to do either at your meetup or event, [get in
|
||||||
|
touch][13].
|
||||||
|
|
||||||
|
[0]: https://github.com/opdavies/drupal-testing-workshop
|
||||||
|
[1]: https://github.com/drupal-composer/drupal-project
|
||||||
|
[2]: https://docksal.io
|
||||||
|
|
||||||
|
[3]: {{site.companies['microserve'].url}} [4]:
|
||||||
|
https://github.com/drupal-composer/drupal-project [5]:
|
||||||
|
https://www.drupal.org/project/examples [6]:
|
||||||
|
/articles/creating-a-custom-phpunit-command-for-docksal [7]:
|
||||||
|
/articles/using-environment-variables-settings-docksal [8]:
|
||||||
|
https://github.com/opdavies/drupal-testing-workshop-exercises [9]:
|
||||||
|
/talks/tdd-test-driven-drupal [10]:
|
||||||
|
https://www.drupal.org/project/private_message_queue [11]:
|
||||||
|
https://www.drupal.org/project/system_user [12]:
|
||||||
|
{{site.events.drupal_dev_days_18.url}} [13]: /contact [14]:
|
||||||
|
{{site.events.drupal_bristol.url}} [15]: /test-driven-drupal [16]:
|
||||||
|
https://groups.drupal.org/node/520891
|
33
source/_posts/drupal-vm-generator-updates.md
Normal file
33
source/_posts/drupal-vm-generator-updates.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
title: Drupal VM Generator 2.9.1 Released
|
||||||
|
date: 2016-12-30
|
||||||
|
excerpt: I’ve released some new versions of the Drupal VM Generator.
|
||||||
|
tags: ['drupal-vm-generator', releases]
|
||||||
|
---
|
||||||
|
|
||||||
|
The main updates are:
|
||||||
|
|
||||||
|
- Fixed an `InvalidResponseException` that was thrown from within the
|
||||||
|
`boolean_as_string` Twig filter from the opdavies/twig-extensions library when
|
||||||
|
the `config:generate` command was run in non-interactive mode.
|
||||||
|
- Adding a working test suite for the existing commands, using PhpUnit and
|
||||||
|
Symfony’s Process component. This is now linked to [Travis CI][2], and the
|
||||||
|
tests are run on each commit and pull request.
|
||||||
|
- The version requirements have been changed to allow 2.7 versions of the used
|
||||||
|
Symfony Components, as well as the 3.x versions. This was done to resolve a
|
||||||
|
conflict when also installing Drush globally with Composer.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Currently the project is based on Drupal VM 3.0.0 which is an outdated version
|
||||||
|
([4.1.0][3] was released today). Adding updates and supporting the newer
|
||||||
|
versions is a high priority, as well as keeping in sync with new releases. This
|
||||||
|
will be easier with the test suite in place.
|
||||||
|
|
||||||
|
My initial thoughts are that version 2.10.0 will support Drupal VM 4.0.0, and if
|
||||||
|
needed, 2.11.0 will ship shortly afterwards and support Drupal VM 4.1.0.
|
||||||
|
|
||||||
|
[0]: http://www.drupalvmgenerator.com
|
||||||
|
[1]: https://github.com/opdavies/drupal-vm-generator/tree/master/tests/Command
|
||||||
|
[2]: https://travis-ci.org/opdavies/drupal-vm-generator
|
||||||
|
[3]: https://github.com/geerlingguy/drupal-vm/releases/tag/4.1.0
|
48
source/_posts/drupalcamp-bristol-2018.md
Normal file
48
source/_posts/drupalcamp-bristol-2018.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
title: DrupalCamp Bristol 2018 Statement
|
||||||
|
date: 2018-01-30
|
||||||
|
excerpt: Unfortunately, we won’t be running DrupalCamp Bristol this year.
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- drupalcamp-bristol
|
||||||
|
use:
|
||||||
|
- posts
|
||||||
|
meta:
|
||||||
|
image:
|
||||||
|
url: /images/blog/drupalcamp-bristol-17-logo.jpg
|
||||||
|
width: 228
|
||||||
|
height: 448
|
||||||
|
type: image/img
|
||||||
|
---
|
||||||
|
|
||||||
|
It’s with heavy hearts that we are announcing there won’t be a DrupalCamp
|
||||||
|
Bristol 2018. The committee have looked at the amount of work required to put
|
||||||
|
the camp on and the capacity we all have and the two numbers are irreconcilable.
|
||||||
|
|
||||||
|
Seeing Drupalists from all over the country and from overseas come to Bristol to
|
||||||
|
share knowledge and ideas is something we take pride in. The past three camps
|
||||||
|
have been fantastic, but as a trend we have left it later and later to organise.
|
||||||
|
|
||||||
|
This year is the latest we have left to organise and we believe this is because
|
||||||
|
we are all a bit fatigued right now, so it seems like a good place to stop and
|
||||||
|
take stock.
|
||||||
|
|
||||||
|
In our washup of last year’s camp we spoke a lot about what DrupalCamp is and
|
||||||
|
who it is for. Traditionally we have tried to get a good mix of speakers from
|
||||||
|
within the Drupal community and from the wider tech community. This does mean we
|
||||||
|
dilute the ‘Drupal’ aspect of the camp, but the benefits it brings in terms of
|
||||||
|
bringing together different views gives the camp greater value in our eyes.
|
||||||
|
|
||||||
|
It’s because of this mix of talks and wider shifts in the community in ‘getting
|
||||||
|
us off the island’ that we have been thinking about rebranding to reflect the
|
||||||
|
mix of talks that the camp hosts. The fact is DrupalCamps don’t just cover
|
||||||
|
Drupal anymore. There is Symfony, Composer, OOP principles, React, etc.
|
||||||
|
|
||||||
|
We’ll take the gap this year to reevaluate who DrupalCamp Bristol is for and
|
||||||
|
where it fits into the schedule of excellent tech events that take place in
|
||||||
|
Bristol through the year, and we look forward to seeing you in 2019, refreshed
|
||||||
|
and more enthusiastic than ever!
|
||||||
|
|
||||||
|
The DrupalCamp Bristol organising committee
|
||||||
|
|
||||||
|
Tom, Ollie, Emily, Sophie, Rob, Mark
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
title: Speakers and sessions announced for DrupalCamp Bristol 2019
|
||||||
|
date: 2019-05-31
|
||||||
|
excerpt:
|
||||||
|
DrupalCamp Bristol is returning next month, and the accepted speakers and
|
||||||
|
sessions have just been announced.
|
||||||
|
tags: [drupalcamp, drupalcamp-bristol, dcbristol]
|
||||||
|
---
|
||||||
|
|
||||||
|
<p class="lead" markdown="1">DrupalCamp Bristol is returning next month for a one-day, single-track conference, and we have just finished announcing the accepted sessions and speakers. It includes a mixture of new and returning speakers, presenting sessions including **Drupal in a microservice architecture**, **Automate to manage repetitive tasks with Ansible** and **Doing good with Drupal**.</p>
|
||||||
|
|
||||||
|
Find out more about all of our sessions and speakers on [the DrupalCamp Bristol
|
||||||
|
website][website], as well as view the schedule for the day.
|
||||||
|
|
||||||
|
Also, at the time of writing, [early bird tickets are still available][tickets]
|
||||||
|
for a few more days!
|
||||||
|
|
||||||
|
In the meantime, the videos from the 2017 Camp are on [our YouTube
|
||||||
|
channel][youtube], including the opening keynote from [Emma Karayiannis][emma]:
|
||||||
|
|
||||||
|
{% include 'video-embed' with {
|
||||||
|
classes: 'video-full',
|
||||||
|
video: {
|
||||||
|
id: 'honnav4YlAA',
|
||||||
|
attr: {
|
||||||
|
height: '315',
|
||||||
|
width: '560',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} %}
|
||||||
|
|
||||||
|
[emma]: https://twitter.com/embobmaria
|
||||||
|
[tickets]: https://2019.drupalcampbristol.co.uk/tickets
|
||||||
|
[website]: https://2019.drupalcampbristol.co.uk
|
||||||
|
[youtube]: https://opdavi.es/dcbristol17-videos
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
title: DrupalCamp Bristol 2017 - Early Bird Tickets, Call for Sessions, Sponsors
|
||||||
|
date: 2017-05-15
|
||||||
|
excerpt:
|
||||||
|
In less than two months time, DrupalCamp Bristol will be back for our third
|
||||||
|
year.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- drupalcamp
|
||||||
|
- drupalcamp-bristol
|
||||||
|
meta:
|
||||||
|
image:
|
||||||
|
url: /assets/image/blog/drupalcamp-bristol-17-logo.jpg
|
||||||
|
height: 228
|
||||||
|
width: 448
|
||||||
|
type: image/jpg
|
||||||
|
---
|
||||||
|
|
||||||
|
<p class="text-center" markdown="1">![DrupalCamp Bristol 2017 logo](/images/blog/drupalcamp-bristol-17-logo.jpg)</p>
|
||||||
|
|
||||||
|
In less than two months time, [DrupalCamp Bristol][0] will be back for our third
|
||||||
|
year! (July seems to come around quicker each year). This is this year’s
|
||||||
|
schedule and venues:
|
||||||
|
|
||||||
|
- 30th June - CXO (Business) day - [Watershed][1]
|
||||||
|
- 1st July - Developer conference - [University of Bristol, School of
|
||||||
|
Chemistry][2]
|
||||||
|
- 2nd July - Contribution sprints - Venue TBC
|
||||||
|
|
||||||
|
Today we announced [Emma Karayiannis][3] as our Saturday keynote speaker, and
|
||||||
|
we’ll be announcing some of the other speakers later this week.
|
||||||
|
|
||||||
|
Not submitted your session yet? The [session submissions][12] are open until May
|
||||||
|
31st. We’re looking for talks not only on Drupal, but other related topics such
|
||||||
|
as PHP, Symfony, server administration/DevOps, project management, case studies,
|
||||||
|
being human etc. If you want to submit but want to ask something beforehand,
|
||||||
|
please [send us an email][4] or ping us on [Twitter][5].
|
||||||
|
|
||||||
|
Not spoken at a DrupalCamp before? No problem. We’re looking for both new and
|
||||||
|
experienced speakers, and have both long (45 minutes) and short (20 minutes)
|
||||||
|
talk slots available.
|
||||||
|
|
||||||
|
Not bought your tickets yet? [Early bird tickets][10] for the CXO and conference
|
||||||
|
days are still available! The sprint day tickets are free but limited, so do
|
||||||
|
register for a ticket to claim your place.
|
||||||
|
|
||||||
|
We still have [sponsorships opportunities][6] available (big thanks to
|
||||||
|
[Microserve][7], [Deeson][8] and [Proctors][9]) who have already signed up), but
|
||||||
|
be quick if you want to be included in our brochure so that we can get you added
|
||||||
|
before our print deadline! Without our sponsors, putting on this event each year
|
||||||
|
would not be possible.
|
||||||
|
|
||||||
|
Any other questions? Take a look at [our website][0] or get in touch via
|
||||||
|
[Twitter][5] or [email][11].
|
||||||
|
|
||||||
|
[0]: https://2017.drupalcampbristol.co.uk
|
||||||
|
[1]: http://www.watershed.co.uk
|
||||||
|
[2]: http://www.bris.ac.uk/chemistry
|
||||||
|
[3]: http://emmakarayiannis.com
|
||||||
|
[4]: mailto:speakers@drupalcampbristol.co.uk
|
||||||
|
[5]: https://twitter.com/DrupalCampBris
|
||||||
|
[6]: https://2017.drupalcampbristol.co.uk/sponsorship
|
||||||
|
[7]: https://microserve.io
|
||||||
|
[8]: https://www.deeson.co.uk
|
||||||
|
[9]: http://www.proctors.co.uk
|
||||||
|
[10]:
|
||||||
|
https://www.eventbrite.co.uk/e/drupalcamp-bristol-2017-tickets-33574193316#ticket
|
||||||
|
[11]: mailto:info@drupalcampbristol.co.uk
|
||||||
|
[12]: https://2017.drupalcampbristol.co.uk/#block-dcb2017-page-title
|
24
source/_posts/drupalcamp-london-2014.md
Normal file
24
source/_posts/drupalcamp-london-2014.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: DrupalCamp London 2014
|
||||||
|
date: 2014-02-09
|
||||||
|
excerpt: It's all booked, I'm going to be attending DrupalCamp London.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupalcamp-london
|
||||||
|
- git
|
||||||
|
- git-flow
|
||||||
|
---
|
||||||
|
|
||||||
|
It's all booked, I'm going to be attending
|
||||||
|
[DrupalCamp London](http://2014.drupalcamplondon.co.uk) this year, my first
|
||||||
|
DrupalCamp!
|
||||||
|
|
||||||
|
I'm going as a volunteer, so I'm going to be helping with the registrations on
|
||||||
|
the Saturday morning and for another couple hours elsewhere over the weekend.
|
||||||
|
I've also offered to help organise and oversee some code sprints, although I'm
|
||||||
|
definitely wanting to do some sprinting of my own and attend a few sessions.
|
||||||
|
|
||||||
|
I'm looking forward to meeting some new people as well as catching up with some
|
||||||
|
people that I met at [DrupalCon Prague](http://prague2013.drupal.org).
|
||||||
|
|
||||||
|
If you're also coming, see you there!
|
41
source/_posts/drupalcamp-london-2019-tickets.md
Normal file
41
source/_posts/drupalcamp-london-2019-tickets.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
title: DrupalCamp London 2019 - Tickets Available and Call for Sessions
|
||||||
|
date: 2018-11-20
|
||||||
|
excerpt:
|
||||||
|
DrupalCamp London early-bird tickets are now available, and their call for
|
||||||
|
sessions is open.
|
||||||
|
tags:
|
||||||
|
- conferences
|
||||||
|
- drupal
|
||||||
|
- drupalcamp
|
||||||
|
- drupalcamp-london
|
||||||
|
has_tweets: true
|
||||||
|
---
|
||||||
|
|
||||||
|
It was announced this week that [early-bird tickets are now available][0] for
|
||||||
|
[DrupalCamp London 2019][1], as well as their [call for sessions being open][2].
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
content: '<p lang="en" dir="ltr">The time is finally here. You can now purchase your tickets. Early Bird finishes on 2nd January 2019 - <a href="https://t.co/aG6jstmWzv">https://t.co/aG6jstmWzv</a> <a href="https://twitter.com/hashtag/Drupal?src=hash&ref_src=twsrc%5Etfw">#Drupal</a></p>— DrupalCamp London (@DrupalCampLDN) <a href="https://twitter.com/DrupalCampLDN/status/1064584179113971712?ref_src=twsrc%5Etfw">November 19, 2018</a>
|
||||||
|
',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
I’ve attended, given talks and volunteered previously, would definitely
|
||||||
|
recommend others doing so, and I plan on attending and submitting again myself
|
||||||
|
for 2019. If there’s something in particular that you’d like to see me give a
|
||||||
|
talk on, let me know - I’d be happy to hear any suggestions. Alternatively, if
|
||||||
|
you’d like to submit and would like some help writing an abstract or want some
|
||||||
|
feedback on a talk idea, please get in touch.
|
||||||
|
|
||||||
|
_Note: I am not an organiser of DrupalCamp London, nor am I involved with the
|
||||||
|
session selection process._
|
||||||
|
|
||||||
|
Hopefully there will be no [#uksnow][3] this year!
|
||||||
|
|
||||||
|
DrupalCamp London is the 1-3 March 2019. Early bird tickets are available until
|
||||||
|
2 January 2019, and the call for sessions is open until 21 January.
|
||||||
|
|
||||||
|
[0]: https://twitter.com/DrupalCampLDN/status/1064584179113971712
|
||||||
|
[1]: https://drupalcamp.london
|
||||||
|
[2]: https://drupalcamp.london/get-involved/submit-a-session
|
||||||
|
[3]: /articles/tweets-drupalcamp-london
|
26
source/_posts/drupalcamp-london-testing-workshop.md
Normal file
26
source/_posts/drupalcamp-london-testing-workshop.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
title: Testing Workshop at DrupalCamp London 2020
|
||||||
|
excerpt: This year, I’m teaching a workshop at DrupalCamp London.
|
||||||
|
tags: [drupal, drupalcamp, testing]
|
||||||
|
date: 2020-02-05
|
||||||
|
lead_image:
|
||||||
|
url: /images/blog/testing-workshop-drupalcamp-london/lead.jpg
|
||||||
|
---
|
||||||
|
|
||||||
|
![]({{page.lead_image.url}}){.p-1 .border}
|
||||||
|
|
||||||
|
<p>{{ page.excerpt }}</p>
|
||||||
|
|
||||||
|
The subject will be automated testing and test driven development in Drupal 8,
|
||||||
|
and it will be on Friday 13th March 2020, between 1pm and 5pm.
|
||||||
|
|
||||||
|
In the workshop, I’ll cover the methodology, approaches and terminology involved
|
||||||
|
with automated testing, look at some examples and work through some exercises,
|
||||||
|
and then take a test driven development approach to creating a new Drupal
|
||||||
|
module.
|
||||||
|
|
||||||
|
There are also other workshops on topics including Composer, Drupal Commerce,
|
||||||
|
profiling, and chatbots.
|
||||||
|
|
||||||
|
For more information and to register, go to the
|
||||||
|
[DrupalCamp London website](https://opdavi.es/dclondon20 'Find out more and register on the DrupalCamp London website').
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
title: Easier Git Repository Cloning with insteadOf
|
||||||
|
date: 2019-03-07
|
||||||
|
excerpt:
|
||||||
|
How to simplify 'git clone' commands by using the insteadOf configuration
|
||||||
|
option within your .gitconfig file.
|
||||||
|
tags: [git]
|
||||||
|
---
|
||||||
|
|
||||||
|
When working on client or open source projects, I clone a lot of
|
||||||
|
[Git](https://git-scm.com) repositories - either from GitHub, GitLab, Bitbucket
|
||||||
|
or Drupal.org. The standard `git clone` commands though provided by these sites
|
||||||
|
can be quite verbose with long repository URLs and use a mixture of different
|
||||||
|
protocols, and I’d regularly need to go back to each website and look up the
|
||||||
|
necessary command every time.
|
||||||
|
|
||||||
|
For example, here is the command provided to clone Drupal’s
|
||||||
|
[Override Node Options module](https://www.drupal.org/project/override_node_options):
|
||||||
|
|
||||||
|
```plain
|
||||||
|
git clone --branch 8.x-2.x https://git.drupal.org/project/override_node_options.git
|
||||||
|
```
|
||||||
|
|
||||||
|
We can though simplify the command to make it easier and quicker to type, using
|
||||||
|
a Git configuration option called `insteadOf`.
|
||||||
|
|
||||||
|
## What is insteadOf?
|
||||||
|
|
||||||
|
From the
|
||||||
|
[Git documentation](https://git-scm.com/docs/git-config#git-config-urlltbasegtinsteadOf):
|
||||||
|
|
||||||
|
> **url.[base].insteadOf:**
|
||||||
|
>
|
||||||
|
> Any URL that starts with this value will be rewritten to start, instead, with
|
||||||
|
> [base]. In cases where some site serves a large number of repositories, and
|
||||||
|
> serves them with multiple access methods, and some users need to use different
|
||||||
|
> access methods, this feature allows people to specify any of the equivalent
|
||||||
|
> URLs and have Git automatically rewrite the URL to the best alternative for
|
||||||
|
> the particular user, even for a never-before-seen repository on the site. When
|
||||||
|
> more than one insteadOf strings match a given URL, the longest match is used.
|
||||||
|
|
||||||
|
Whilst examples are sparse,
|
||||||
|
[it seems like](https://stackoverflow.com/questions/1722807/how-to-convert-git-urls-to-http-urls)
|
||||||
|
insteadOf is used for resolving protocol issues with repository URLs. However,
|
||||||
|
we can use it to simplify our clone commands, as mentioned above.
|
||||||
|
|
||||||
|
## Example: cloning Drupal contrib projects
|
||||||
|
|
||||||
|
When working on Drupal core, or on a module, theme or distribution, you need to
|
||||||
|
have a cloned version of that repository to generate patch files from, and apply
|
||||||
|
patches to.
|
||||||
|
|
||||||
|
Again, here is the provided command to clone the Override Node Options module:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
git clone --branch 8.x-2.x https://git.drupal.org/project/override_node_options.git
|
||||||
|
```
|
||||||
|
|
||||||
|
At the time of writing, the Git repository URL follow this same format -
|
||||||
|
`https://git.drupal.org/project/{name}.git` (also the `.git` file extension is
|
||||||
|
optional).
|
||||||
|
|
||||||
|
To shorten and simplify this, I can add this snippet to my `~/.gitconfig` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[url "https://git.drupal.org/project/"]
|
||||||
|
insteadOf = do:
|
||||||
|
insteadOf = drupal:
|
||||||
|
```
|
||||||
|
|
||||||
|
With that added, I can now instead run `git clone drupal:{name}` or
|
||||||
|
`git clone do:{name}` to clone the repository, specifying the project’s machine
|
||||||
|
name.
|
||||||
|
|
||||||
|
For example, to clone the Override Node Options module, I can now do this using
|
||||||
|
just `git clone drupal:override_node_options`.
|
||||||
|
|
||||||
|
This, I think, is definitely quicker and easier!
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
You can view my entire `.gitconfig` file, as well as my other dotfiles, in
|
||||||
|
[my dotfiles repository on GitHub](https://github.com/opdavies/dotfiles/blob/master/.gitconfig).
|
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
title: Easier Sculpin Commands with Composer and NPM Scripts
|
||||||
|
date: 2017-01-07
|
||||||
|
excerpt:
|
||||||
|
In this video, I show you how I've simplied my Sculpin and Gulp workflow using
|
||||||
|
custom Composer and NPM scripts.
|
||||||
|
tags: [composer, gulp, sculpin]
|
||||||
|
---
|
||||||
|
|
||||||
|
In this video, I show you how I've simplied my Sculpin and Gulp workflow using
|
||||||
|
custom Composer and NPM scripts.
|
||||||
|
|
||||||
|
My website includes several various command line tools - e.g. [Sculpin][4],
|
||||||
|
[Gulp][5] and [Behat][6] - each needing different arguments and options,
|
||||||
|
depending on the command being run. For example, for Sculpin, I normally include
|
||||||
|
several additional options when viewing the site locally - the full command that
|
||||||
|
I use is
|
||||||
|
`./vendor/bin/sculpin generate --watch --server --clean --no-interaction`.
|
||||||
|
Typing this repeatedly is time consuming and could be easily mis-typed,
|
||||||
|
forgotten or confused with other commands.
|
||||||
|
|
||||||
|
In this video, I show you how I've simplied my Sculpin and Gulp workflow using
|
||||||
|
custom Composer and NPM scripts.
|
||||||
|
|
||||||
|
<div class="embed-container">
|
||||||
|
<iframe width="560" height="315" src="https://www.youtube.com/embed/eiWDV_63yCQ" frameborder="0" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
Here are the scripts that I’m using - they are slightly different from those in
|
||||||
|
the video. I use the `--generate` and `--watch` options for Sculpin and the
|
||||||
|
`gulp watch` command for NPM. I had to change these before the recording as I
|
||||||
|
was using the [demo magic][0] script to run the commands, and existing from a
|
||||||
|
watch session was also ending the script process.
|
||||||
|
|
||||||
|
### composer.json
|
||||||
|
|
||||||
|
```language-json
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf output_*/",
|
||||||
|
"dev": "sculpin generate --clean --no-interaction --server --watch",
|
||||||
|
"production": "sculpin generate --clean --no-interaction --env='prod' --quiet"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with `composer run <name>`, e.g. `composer run dev`.
|
||||||
|
|
||||||
|
### package.json
|
||||||
|
|
||||||
|
```language-json
|
||||||
|
"scripts": {
|
||||||
|
"init": "yarn && bower install",
|
||||||
|
"dev": "gulp watch",
|
||||||
|
"production": "gulp --production"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with `npm run <name>`, e.g. `npm run production`.
|
||||||
|
|
||||||
|
You can also take a look at the full [composer.json][1] and [package.json][2]
|
||||||
|
files within my site repository on [GitHub][3].
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Composer scripts][7]
|
||||||
|
- [oliverdavies.uk composer.json][1]
|
||||||
|
- [oliverdavies.uk package.json][2]
|
||||||
|
|
||||||
|
[0]: https://github.com/paxtonhare/demo-magic
|
||||||
|
[1]: https://github.com/opdavies/oliverdavies.uk/blob/master/composer.json
|
||||||
|
[2]: https://github.com/opdavies/oliverdavies.uk/blob/master/package.json
|
||||||
|
[3]: https://github.com/opdavies/oliverdavies.uk
|
||||||
|
[4]: https://sculpin.io
|
||||||
|
[5]: http://gulpjs.com
|
||||||
|
[6]: http://behat.org
|
||||||
|
[7]: https://getcomposer.org/doc/04-schema.md#scripts
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
title: Easily Embed TypeKit Fonts into your Drupal Website
|
||||||
|
date: 2011-02-14
|
||||||
|
excerpt:
|
||||||
|
How to use the @font-your-face module to embed TypeKit fonts into your Drupal
|
||||||
|
website.
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- typekit
|
||||||
|
---
|
||||||
|
|
||||||
|
To begin with, you will need to
|
||||||
|
[register for a TypeKit account](https://typekit.com/plans) - there is a free
|
||||||
|
version if you just want to try it out.
|
||||||
|
|
||||||
|
Next, you'll need to create a kit that contains the fonts that you want to use
|
||||||
|
on your website. I've used
|
||||||
|
[FF Tisa Web Pro](http://typekit.com/fonts/ff-tisa-web-pro).
|
||||||
|
|
||||||
|
Under 'Kit Settings', ensure that your website domain (e.g. mysite.com) is
|
||||||
|
listed under 'Domains'.
|
||||||
|
|
||||||
|
Download and install the
|
||||||
|
[@font-your-face](http://drupal.org/project/fontyourface) module onto your
|
||||||
|
Drupal site, and to go admin/settings/fontyourface to configure it. Enter
|
||||||
|
[your TypeKit API key](https://typekit.com/account/tokens), and click 'Import
|
||||||
|
Typekit' to import your kits and fonts.
|
||||||
|
|
||||||
|
Go to admin/dist/themes/fontyourface, and click 'Browse fonts to enable'. Click
|
||||||
|
on the name of the font that you want to enable, check 'Enabled', and click
|
||||||
|
'Edit font' to save your changes.
|
||||||
|
|
||||||
|
With the font enabled, you can now use it in your CSS.
|
63
source/_posts/entityform.md
Normal file
63
source/_posts/entityform.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
title: Programmatically Load an Entityform in Drupal 7
|
||||||
|
date: 2015-12-22
|
||||||
|
excerpt:
|
||||||
|
How to programmatically load, render and embed an entityform in Drupal 7.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
- entityform
|
||||||
|
---
|
||||||
|
|
||||||
|
I recently had my first experience using the
|
||||||
|
[Entityform module](https://www.drupal.org/project/entityform) in a project. It
|
||||||
|
was quite easy to configure with different form types, but then I needed to
|
||||||
|
embed the form into an overlay. I was expecting to use the `drupal_get_form()`
|
||||||
|
function and render it, but this didn’t work.
|
||||||
|
|
||||||
|
Here are the steps that I took to be able to load, render and embed the form.
|
||||||
|
|
||||||
|
## Loading the Form
|
||||||
|
|
||||||
|
The first thing that I needed to do to render the form was to load an empty
|
||||||
|
instance of the entityform using `entityform_empty_load()`. In this example,
|
||||||
|
`newsletter` is the name of my form type.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$form = entityform_empty_load('newsletter');
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns an instance of a relevant `Entityform` object.
|
||||||
|
|
||||||
|
## Rendering the Form
|
||||||
|
|
||||||
|
The next step was to be able to render the form. I did this using the
|
||||||
|
`entity_form_wrapper()` function.
|
||||||
|
|
||||||
|
As this function is within the `entityform.admin.inc` file and not autoloaded by
|
||||||
|
Drupal, I needed to include it using `module_load_include()` so that the
|
||||||
|
function was available.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
module_load_include('inc', 'entityform', 'entityform.admin');
|
||||||
|
|
||||||
|
$output = entityform_form_wrapper($form, 'submit', 'embedded'),
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument is the `Entityform` object that was created in the previous
|
||||||
|
step (I’ve [submitted a patch](https://www.drupal.org/node/2639584) to type hint
|
||||||
|
this within entityform so that it’s clearer what is expected), which is
|
||||||
|
required.
|
||||||
|
|
||||||
|
The other two arguments are optional. The second argument is the mode (`submit`
|
||||||
|
is the default value), and the last is the form context. `page` is the default
|
||||||
|
value, for use on the submit page, however I changed this to `embedded`.
|
||||||
|
|
||||||
|
I could then pass this result into my theme function to render it successfully
|
||||||
|
within the relevant template file.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [The entityform module](https://www.drupal.org/project/entityform)
|
||||||
|
- [My issue and patch to add the type hint to the entityform_form_wrapper function](https://www.drupal.org/node/2639584)
|
73
source/_posts/examples-of-laravel-collections-in-drupal.md
Normal file
73
source/_posts/examples-of-laravel-collections-in-drupal.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
title: Examples of using Laravel Collections in Drupal
|
||||||
|
date: 2018-08-23
|
||||||
|
excerpt:
|
||||||
|
Some examples of using Laravel’s Illuminate Collections within Drupal
|
||||||
|
projects.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-7
|
||||||
|
- drupal-8
|
||||||
|
- drupal-planet
|
||||||
|
- laravel
|
||||||
|
- laravel-collections
|
||||||
|
- php
|
||||||
|
has_tweets: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Since starting to work with Laravel as well as Drupal and Symfony, watching Adam
|
||||||
|
Wathan’s [Refactoring to Collections][0] course as well as [lessons on
|
||||||
|
Laracasts][6], I’ve become a fan of [Laravel’s Illuminate Collections][1] and
|
||||||
|
the object-orientated pipeline approach for interacting with PHP arrays.
|
||||||
|
|
||||||
|
In fact I’ve given a talk on [using Collections outside Laravel][2] and have
|
||||||
|
written a [Collection class module][3] for Drupal 7.
|
||||||
|
|
||||||
|
I’ve also tweeted several examples of code that I’ve written within Drupal that
|
||||||
|
use Collections, and I thought it would be good to collate them all here for
|
||||||
|
reference.
|
||||||
|
|
||||||
|
Thanks again to [Tighten][4] for releasing and maintaining the
|
||||||
|
[tightenco/collect library][5] that makes it possible to pull in Collections via
|
||||||
|
Composer.
|
||||||
|
|
||||||
|
<div class="lg:flex lg:flex-wrap lg:-mx-4">
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'block mb-4 lg:w-1/2 lg:px-2 lg:mb-0',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Putting <a href="https://twitter.com/laravelphp?ref_src=twsrc%5Etfw">@laravelphp</a>'s Collection class to good use, cleaning up some of my <a href="https://twitter.com/drupal?ref_src=twsrc%5Etfw">@drupal</a> 8 code. Thanks <a href="https://twitter.com/TightenCo?ref_src=twsrc%5Etfw">@TightenCo</a> for the Collect library! <a href="https://t.co/Bn1UfudGvp">pic.twitter.com/Bn1UfudGvp</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/898577157193998337?ref_src=twsrc%5Etfw">August 18, 2017</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'block mb-4 lg:w-1/2 lg:px-2 lg:mb-0',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Putting more <a href="https://twitter.com/laravelphp?ref_src=twsrc%5Etfw">@laravelphp</a> Collections to work in my <a href="https://twitter.com/drupal?ref_src=twsrc%5Etfw">@drupal</a> code today. 😁 <a href="https://t.co/H8xDTT063X">pic.twitter.com/H8xDTT063X</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/963890078933282817?ref_src=twsrc%5Etfw">February 14, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'block mb-4 lg:w-1/2 lg:px-2 lg:mb-0',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">I knew that you could specify a property like 'price' in Twig and it would also look for methods like 'getPrice()', but I didn't know (or had maybe forgotten) that <a href="https://twitter.com/laravelphp?ref_src=twsrc%5Etfw">@laravelphp</a> Collections does it too.<br><br>This means that these two Collections return the same result.<br><br>Nice! 😎 <a href="https://t.co/2g2IfThzdy">pic.twitter.com/2g2IfThzdy</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1009451206765416448?ref_src=twsrc%5Etfw">June 20, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'block mb-4 lg:w-1/2 lg:px-2 lg:mb-0',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">More <a href="https://twitter.com/laravelphp?ref_src=twsrc%5Etfw">@laravelphp</a> Collection goodness, within my <a href="https://twitter.com/hashtag/Drupal8?src=hash&ref_src=twsrc%5Etfw">#Drupal8</a> project! <a href="https://t.co/mWgpNbNIrh">pic.twitter.com/mWgpNbNIrh</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1027843931101380608?ref_src=twsrc%5Etfw">August 10, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% include 'tweet' with {
|
||||||
|
class: 'block mb-4 lg:w-1/2 lg:px-2 lg:mb-0',
|
||||||
|
data_cards: true,
|
||||||
|
content: '<p lang="en" dir="ltr">Some more <a href="https://twitter.com/hashtag/Drupal?src=hash&ref_src=twsrc%5Etfw">#Drupal</a> 8 fun with Laravel Collections. Loading the tags for a post and generating a formatted string of tweetable hashtags. <a href="https://t.co/GbyiRPzIRo">pic.twitter.com/GbyiRPzIRo</a></p>— Oliver Davies (@opdavies) <a href="https://twitter.com/opdavies/status/1032544228029673472?ref_src=twsrc%5Etfw">August 23, 2018</a>',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[0]: https://adamwathan.me/refactoring-to-collections
|
||||||
|
[1]: https://laravel.com/docs/collections
|
||||||
|
[2]: /talks/using-laravel-collections-outside-laravel
|
||||||
|
[3]: https://www.drupal.org/project/collection_class
|
||||||
|
[4]: https://tighten.co
|
||||||
|
[5]: https://packagist.org/packages/tightenco/collect
|
||||||
|
[6]: https://laracasts.com/series/how-do-i/episodes/18
|
83
source/_posts/experimenting-with-events-in-drupal-8.md
Normal file
83
source/_posts/experimenting-with-events-in-drupal-8.md
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
title: Experimenting with events in Drupal 8
|
||||||
|
date: 2018-08-21
|
||||||
|
excerpt:
|
||||||
|
Trying a different way of structuring Drupal modules, using event subscribers
|
||||||
|
and autowiring.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-8
|
||||||
|
- drupal-planet
|
||||||
|
- php
|
||||||
|
- symfony
|
||||||
|
promoted: true
|
||||||
|
---
|
||||||
|
|
||||||
|
I’ve been experimenting with moving some code to Drupal 8, and I’m quite
|
||||||
|
intrigued by a different way that I’ve tried to structure it - using event
|
||||||
|
subscribers, building on some of the takeaways from Drupal Dev Days.
|
||||||
|
|
||||||
|
Here is how this module is currently structured:
|
||||||
|
|
||||||
|
![](/images/blog/events-drupal-8/1.png){.border .p-1}
|
||||||
|
|
||||||
|
Note that there is no `opdavies_blog.module` file, and rather than calling
|
||||||
|
actions from within a hook like `opdavies_blog_entity_update()`, each action
|
||||||
|
becomes it’s own event subscriber class.
|
||||||
|
|
||||||
|
This means that there are no long `hook_entity_update` functions, and instead
|
||||||
|
there are descriptive, readable event subscriber class names, simpler action
|
||||||
|
code that is responsibile only for performing one task, and you’re able to
|
||||||
|
inject and autowire dependencies into the event subscriber classes as services -
|
||||||
|
making it easier and cleaner to use dependency injection, and simpler write
|
||||||
|
tests to mock dependencies when needed.
|
||||||
|
|
||||||
|
The additional events are provided by the
|
||||||
|
[Hook Event Dispatcher module](https://www.drupal.org/project/hook_event_dispatcher).
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
`opdavies_blog.services.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
Drupal\opdavies_blog\EventSubscriber\PostToMedium:
|
||||||
|
autowire: true
|
||||||
|
tags:
|
||||||
|
- { name: event_subscriber }
|
||||||
|
|
||||||
|
Drupal\opdavies_blog\EventSubscriber\SendTweet:
|
||||||
|
autowire: true
|
||||||
|
tags:
|
||||||
|
- { name: event_subscriber }
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="note" markdown="1">
|
||||||
|
Adding `autowire: true` is not required for the event subscriber to work. I’m using it to automatically inject any dependencies into the class rather than specifying them separately as arguments.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`src/EventSubscriber/SendTweet.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace Drupal\opdavies_blog\EventSubscriber;
|
||||||
|
|
||||||
|
use Drupal\hook_event_dispatcher\Event\Entity\EntityUpdateEvent;
|
||||||
|
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
class SendTweet implements EventSubscriberInterface {
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
public static function getSubscribedEvents() {
|
||||||
|
return [
|
||||||
|
HookEventDispatcherInterface::ENTITY_UPDATE => 'sendTweet',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendTweet(EntityUpdateEvent $event) {
|
||||||
|
// Perform checks and send the tweet.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
title: Finding the last commit that a patch applies to
|
||||||
|
excerpt: How to find the last commit in a Git repository that a patch applies to.
|
||||||
|
date: 2020-03-26
|
||||||
|
tags: [bash, git]
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# https://www.drupal.org/files/issues/2018-08-28/group-configurable-entities-as-group-content-2797793-58.patch
|
||||||
|
|
||||||
|
patch_filename=group-configurable-entities-as-group-content-2797793-58.patch
|
||||||
|
first_commit=6e8c22a
|
||||||
|
last_commit=8.x-1.x
|
||||||
|
|
||||||
|
find_commits_between() {
|
||||||
|
first_commit=$1
|
||||||
|
last_commit=$2
|
||||||
|
|
||||||
|
git rev-list --reverse --ancestry-path $first_commit^...$last_commit
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_repo() {
|
||||||
|
git reset --hard $1 >& /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_patch() {
|
||||||
|
git apply --check $patch_filename >& /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
for sha1 in $(find_commits_between $first_commit $last_commit); do
|
||||||
|
echo "Trying ${sha1}..."
|
||||||
|
|
||||||
|
reset_repo $sha1
|
||||||
|
apply_patch
|
||||||
|
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
echo "Patch applies"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Patch does not apply"
|
||||||
|
exit 1
|
||||||
|
done
|
||||||
|
```
|
28
source/_posts/fix-vagrant-loading-wrong-virtual-machine.md
Normal file
28
source/_posts/fix-vagrant-loading-wrong-virtual-machine.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
title: How to fix Vagrant Loading the Wrong Virtual Machine
|
||||||
|
date: 2014-10-06
|
||||||
|
excerpt:
|
||||||
|
Here are the steps that I took to fix Vagrant and point it back at the correct
|
||||||
|
VM.
|
||||||
|
tags:
|
||||||
|
- vagrant
|
||||||
|
- virtualbox
|
||||||
|
---
|
||||||
|
|
||||||
|
A few times recently, I've had instances where
|
||||||
|
[Vagrant](https://www.vagrantup.com) seems to have forgotten which virtual
|
||||||
|
machine it's supposed to load, probably due to renaming a project directory or
|
||||||
|
the .vagrant directory being moved accidentally.
|
||||||
|
|
||||||
|
Here are the steps that I took to fix this and point Vagrant back at the correct
|
||||||
|
VM.
|
||||||
|
|
||||||
|
1. Stop the machine from running using the `$ vagrant halt` command.
|
||||||
|
1. Use the `$ VBoxManage list vms` command to view a list of the virtual
|
||||||
|
machines on your system. Note the ID of the correct VM that should be
|
||||||
|
loading. For example,
|
||||||
|
`"foo_default_1405481857614_74478" {e492bfc3-cac2-4cde-a396-e81e37e421e2}`.
|
||||||
|
The number within the curly brackets is the ID of the virtual machine.
|
||||||
|
1. Within the .vagrant directory in your project (it is hidden by default),
|
||||||
|
update the ID within the machines/default/virtualbox/id file.
|
||||||
|
1. Start the new VM with `$ vagrant up`.
|
132
source/_posts/fixing-drupal-simpletest-docker.md
Normal file
132
source/_posts/fixing-drupal-simpletest-docker.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
title: Fixing Drupal SimpleTest issues inside Docker Containers
|
||||||
|
date: 2017-05-05
|
||||||
|
excerpt:
|
||||||
|
How I managed to get my Drupal SimpleTest tests to run and pass within Docker
|
||||||
|
containers.
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- simpletest
|
||||||
|
- testing
|
||||||
|
---
|
||||||
|
|
||||||
|
I’ve been a Drupal VM user for a long time, but lately I’ve been using a
|
||||||
|
combination Drupal VM and Docker for my local development environment. There
|
||||||
|
were a couple of issues preventing me from completely switching to Docker - one
|
||||||
|
of which being that when I tried running of my Simpletest tests, a lot of them
|
||||||
|
would fail where they would pass when run within Drupal VM.
|
||||||
|
|
||||||
|
Here’s an excerpt from my `docker-compose.yml` file:
|
||||||
|
|
||||||
|
**TL;DR** You need to include the name of your web server container as the
|
||||||
|
`--url` option to `run-scripts.php`.
|
||||||
|
|
||||||
|
I’ve been a [Drupal VM][1] user for a long time, but lately I’ve been using a
|
||||||
|
combination Drupal VM and [Docker][0] for my local development environment.
|
||||||
|
There were a couple of issues preventing me from completely switching to
|
||||||
|
Docker - one of which being that when I tried running of my Simpletest tests, a
|
||||||
|
lot of them would fail where they would pass when run within Drupal VM.
|
||||||
|
|
||||||
|
Here’s an excerpt from my `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```language-yaml
|
||||||
|
services:
|
||||||
|
php:
|
||||||
|
image: wodby/drupal-php:5.6
|
||||||
|
volumes:
|
||||||
|
- ./repo:/var/www/html
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: wodby/drupal-nginx:7-1.10
|
||||||
|
environment:
|
||||||
|
NGINX_BACKEND_HOST: php
|
||||||
|
NGINX_SERVER_ROOT: /var/www/html/web
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes_from:
|
||||||
|
- php
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Nginx and PHP-FPM are running in separate containers, the volumes are shared
|
||||||
|
across both and the Nginx backend is set to use the `php` container.
|
||||||
|
|
||||||
|
This is the command that I was using to run the tests:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ docker-compose run --rm \
|
||||||
|
-w /var/www/html/web \
|
||||||
|
php \
|
||||||
|
php scripts/run-tests.sh \
|
||||||
|
--php /usr/local/bin/php \
|
||||||
|
--class OverrideNodeOptionsTestCase
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a new instance of the `php` container, sets the working directory
|
||||||
|
to my Drupal root and runs Drupal’s `run-tests.sh` script with some arguments.
|
||||||
|
In this case, I'm running the `OverrideNodeOptionsTestCase` class for the
|
||||||
|
override_node_options tests. Once complete, the container is deleted because of
|
||||||
|
the `--rm` option.
|
||||||
|
|
||||||
|
This resulted in 60 of the 112 tests failing, whereas they all passed when run
|
||||||
|
within a Drupal VM instance.
|
||||||
|
|
||||||
|
```language-markup
|
||||||
|
Test summary
|
||||||
|
------------
|
||||||
|
|
||||||
|
Override node options 62 passes, 60 fails, 29 exceptions, and 17 debug messages
|
||||||
|
|
||||||
|
Test run duration: 2 min 25 sec
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the tests again with the`--verbose` option, I saw this message appear in
|
||||||
|
the output below some of the failing tests:
|
||||||
|
|
||||||
|
> simplexml_import_dom(): Invalid Nodetype to import
|
||||||
|
|
||||||
|
\*\*Up After checking that I had all of the required PHP extensions installed, I
|
||||||
|
ran `docker-compose exec php bash` to connect to the `php` container and ran
|
||||||
|
`curl http://localhost` to check the output. Rather than seeing the HTML for the
|
||||||
|
site, I got this error message:
|
||||||
|
|
||||||
|
> curl: (7) Failed to connect to localhost port 80: Connection refused
|
||||||
|
|
||||||
|
Whereas `curl http://nginx` returns the HTML for the page, so included it with
|
||||||
|
the `--url` option to `run-tests.sh`, and this resulted in my tests all passing.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ docker-compose run --rm \
|
||||||
|
-w /var/www/html/web \
|
||||||
|
php \
|
||||||
|
php scripts/run-tests.sh \
|
||||||
|
--php /usr/local/bin/php \
|
||||||
|
--url http://nginx \
|
||||||
|
--class OverrideNodeOptionsTestCase
|
||||||
|
```
|
||||||
|
|
||||||
|
```language-markup
|
||||||
|
Test summary
|
||||||
|
------------
|
||||||
|
|
||||||
|
Override node options 121 passes, 0 fails, 0 exceptions, and 34 debug messages
|
||||||
|
|
||||||
|
Test run duration: 2 min 31 sec
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** In this example I have separate `nginx` and `php` containers, but I've
|
||||||
|
tried and had the same issue when running Nginx and PHP-FPM in the same
|
||||||
|
container - e.g. called `app` - and still needed to add `--url http://app` in
|
||||||
|
order for the tests to run successfully.
|
||||||
|
|
||||||
|
I don’t know if this issue is macOS specfic (I know that [Drupal CI][2] is based
|
||||||
|
on Docker, and I don’t know if it’s an issue) but I’m going to test also on my
|
||||||
|
Ubuntu Desktop environment and investigate further and also compare the test run
|
||||||
|
times for Docker in macOS, Docker in Ubuntu and within Drupal VM. I’m also going
|
||||||
|
to test this with PHPUnit tests with Drupal 8.
|
||||||
|
|
||||||
|
[0]: https://www.docker.com
|
||||||
|
[1]: https://www.drupalvm.com
|
||||||
|
[2]: https://www.drupal.org/drupalorg/docs/drupal-ci
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
title: Forward one domain to another using mod_rewrite and .htaccess
|
||||||
|
date: 2012-05-23
|
||||||
|
excerpt: How to use the .htaccess file to forward to a different domain.
|
||||||
|
tags:
|
||||||
|
- .htaccess
|
||||||
|
- code
|
||||||
|
- drupal
|
||||||
|
- apache
|
||||||
|
- mod_rewrite
|
||||||
|
---
|
||||||
|
|
||||||
|
How to use the .htaccess file to forward to a different domain.
|
||||||
|
|
||||||
|
Within the mod_rewrite section of your .htaccess file, add the following lines:
|
||||||
|
|
||||||
|
```language-apacheconf
|
||||||
|
RewriteCond %{HTTP_HOST} ^yoursite\.co\.uk$
|
||||||
|
RewriteRule (.*) http://yoursite.com/$1 [R=301,L]
|
||||||
|
```
|
||||||
|
|
||||||
|
This automatically forwards any users from http://yoursite.co.uk to
|
||||||
|
http://yoursite.com. This can also be used to forward multiple domains:
|
||||||
|
|
||||||
|
```language-apacheconf
|
||||||
|
RewriteCond %{HTTP_HOST} ^yoursite\.co\.uk$ [OR]
|
||||||
|
RewriteCond %{HTTP_HOST} ^yoursite\.info$ [OR]
|
||||||
|
RewriteCond %{HTTP_HOST} ^yoursite\.biz$ [OR]
|
||||||
|
RewriteCond %{HTTP_HOST} ^yoursite\.eu$
|
||||||
|
RewriteRule (.*) http://yoursite.com/$1 [R=301,L]
|
||||||
|
```
|
||||||
|
|
||||||
|
If any of the RewriteCond conditions apply, then the RewriteRule is executed.
|
138
source/_posts/git-format-patch.md
Normal file
138
source/_posts/git-format-patch.md
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
---
|
||||||
|
title: git format-patch is your Friend
|
||||||
|
date: 2014-05-21
|
||||||
|
excerpt:
|
||||||
|
An explanation of the "git format-patch" command, and how it could be used in
|
||||||
|
Drupal's Git workflow.
|
||||||
|
tags:
|
||||||
|
- patches
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- git
|
||||||
|
---
|
||||||
|
|
||||||
|
An explanation of the "git format-patch" command, and how it could be used in
|
||||||
|
Drupal's Git workflow.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
As an active contributor to the [Drupal](http://drupal.org) project, I spend a
|
||||||
|
lot of time working with other peoples’ modules and themes, and occassionally
|
||||||
|
have to fix a bug or add some new functionality.
|
||||||
|
|
||||||
|
In the Drupal community, we use a patch based workflow where any changes that I
|
||||||
|
make get exported to a file detailing the differences. The patch file (\*.patch)
|
||||||
|
is attached to an item in an issue queue on Drupal.org, applied by the
|
||||||
|
maintainer to their local copy of the code and reviewed, and hopefully
|
||||||
|
committed.
|
||||||
|
|
||||||
|
There is an option that the maintainer can add to the end of their commit
|
||||||
|
message.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
--author="opdavies <opdavies@381388.no-reply.drupal.org>"
|
||||||
|
```
|
||||||
|
|
||||||
|
This differs slightly different for each Drupal user, and the code can be found
|
||||||
|
on their Drupal.org profile page.
|
||||||
|
|
||||||
|
If this is added to the end of the commit message, the resulting commit will
|
||||||
|
show that it was committed by the maintainer but authored by a different user.
|
||||||
|
This will then display on Drupal.org that you’ve made a commit to that project.
|
||||||
|
|
||||||
|
![A screenshot of a commit that was authored by rli but committed by opdavies](/images/blog/git-format-patch.png)
|
||||||
|
|
||||||
|
The problem is that some project maintainers either don’t know about this option
|
||||||
|
or occasionally forget to add it. [Dreditor](http://dreditor.org) can suggest a
|
||||||
|
commit message and assign an author, but it is optional and, of course, not all
|
||||||
|
maintainers use Dreditor (although they probably should).
|
||||||
|
|
||||||
|
The `git format-patch` command seems to be the answer, and will be my preferred
|
||||||
|
method for generating patch files in the future rather than `git diff`.
|
||||||
|
|
||||||
|
## What does it do Differently?
|
||||||
|
|
||||||
|
From the [manual page](http://git-scm.com/docs/git-format-patch):
|
||||||
|
|
||||||
|
> Prepare each commit with its patch in one file per commit, formatted to
|
||||||
|
> resemble UNIX mailbox format. The output of this command is convenient for
|
||||||
|
> e-mail submission or for use with git am.
|
||||||
|
|
||||||
|
Here is a section of a patch that I created for the
|
||||||
|
[Metatag module](http://drupal.org/project/metatag) using `git format-patch`:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
From 80c8fa14de7f4a83c2e70367aab0aedcadf4f3b0 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Oliver Davies <oliver@oliverdavies.co.uk>
|
||||||
|
Subject: [PATCH] Exclude comment entities when checking if this is the page,
|
||||||
|
otherwise comment_fragment.module will break metatag
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
As mentioned above, the patch is structured in an email format. The commit
|
||||||
|
message is used as the subject line, and the date that the commit was made
|
||||||
|
locally is used for the date. What we’re interested in is the “From” value. This
|
||||||
|
contains your name and email address from your `~/.gitconfig` file and is used
|
||||||
|
to author the patch automatically.
|
||||||
|
|
||||||
|
Everything below this is the same as a standard patch file, the same as if was
|
||||||
|
generated with `git diff`.
|
||||||
|
|
||||||
|
The full patch file can be found at
|
||||||
|
<https://drupal.org/files/issues/metatag-comment-fragment-conflict-2265447-4.patch>.
|
||||||
|
|
||||||
|
## The Process
|
||||||
|
|
||||||
|
How did I create this patch? Here are the steps that I took:
|
||||||
|
|
||||||
|
1. Clone the source repository using
|
||||||
|
`$ git clone --branch 7.x-1.x http://git.drupal.org/project/metatag.git` and
|
||||||
|
move into that directory.
|
||||||
|
2. Create a branch for this patch using
|
||||||
|
`$ git checkout -b 2265447-comment-fragment-conflict`.
|
||||||
|
3. Add and commit any changes as normal.
|
||||||
|
4. Generate the patch file using
|
||||||
|
`$ git format-patch 7.x-1.x --stdout > metatag-comment-fragment-conflict-2265447-4.patch`.
|
||||||
|
|
||||||
|
_Note:_ I am defining 7.x-1.x in the last step as the original branch to compare
|
||||||
|
(i.e. the original branch that we forked to make our issue branch). This will
|
||||||
|
change depending on the project that you are patching, and it’s version number.
|
||||||
|
Also, commits should always be made against the development branch and not the
|
||||||
|
stable release.
|
||||||
|
|
||||||
|
By default, a separate patch file will be created for each commit that we’ve
|
||||||
|
made. This is overridden by the `--stdout` option which combines all of the
|
||||||
|
patches into a single file. This is the recommended approach when uploading to
|
||||||
|
Drupal.org.
|
||||||
|
|
||||||
|
The resulting patch file can be uploaded onto a Drupal.org issue queue, reviewed
|
||||||
|
by the Testbot and applied by a module maintainer, and you automatically get the
|
||||||
|
commit attributed. Problem solved.
|
||||||
|
|
||||||
|
## Committing the Patch
|
||||||
|
|
||||||
|
If you need to commit a patch that was created using `git format-patch`, the
|
||||||
|
best command to do this with is the `git am` command.
|
||||||
|
|
||||||
|
For example, within your repository, run:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ git am /path/to/file
|
||||||
|
$ git am ~/Code/metatag-comment-fragment-conflict-2265447-4.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
You should end up with some output similar to the following:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
Applying: #2272799 Added supporters section
|
||||||
|
Applying: #2272799 Added navigation tabs
|
||||||
|
Applying: #2272799 Fixed indentation
|
||||||
|
Applying: #2272799 Replaced URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line is the commit message associated with that patch.
|
||||||
|
|
||||||
|
Assuming that there are no errors, you can go ahead and push your updated code
|
||||||
|
into your remote repository.
|
18
source/_posts/going-drupalcon.md
Normal file
18
source/_posts/going-drupalcon.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
title: Going to DrupalCon
|
||||||
|
date: 2013-07-26
|
||||||
|
excerpt:
|
||||||
|
Precedent are sending myself and two of our other Drupal Developers to
|
||||||
|
Drupalcon Prague.
|
||||||
|
tags:
|
||||||
|
- drupalcon
|
||||||
|
- precedent
|
||||||
|
---
|
||||||
|
|
||||||
|
[Precedent](http://www.precedent.co.uk) are sending myself and two of our other
|
||||||
|
Drupal Developers to [Drupalcon Prague](http://prague2013.drupal.org).
|
||||||
|
|
||||||
|
Having wanted to attend the last few Drupalcons (London, especially) but not
|
||||||
|
being able to, I'm definitely looking forward to this one.
|
||||||
|
|
||||||
|
See you there!
|
35
source/_posts/imagefield-import-archive.md
Normal file
35
source/_posts/imagefield-import-archive.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
title: Imagefield Import Archive
|
||||||
|
date: 2011-05-23
|
||||||
|
excerpt: I've finally uploaded my first module onto Drupal.org!
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- imagefield-import
|
||||||
|
---
|
||||||
|
|
||||||
|
I've finally uploaded my first module onto Drupal.org!
|
||||||
|
|
||||||
|
I've written many custom modules, although the vast majority of them are either
|
||||||
|
small tweaks for my own sites, or company/site-specific modules that wouldn't be
|
||||||
|
good to anyone else, so there would be nothing achieved by contributing them
|
||||||
|
back to the community. Previously, I've blogged about the
|
||||||
|
[Imagefield Import](http://drupal.org/project/imagefield_import) module - a
|
||||||
|
module that I use on a number of sites, both personal and for clients - and what
|
||||||
|
I've looked for lately is for a way to speed up the process again. Gathering my
|
||||||
|
images together and manually copying them into the import directory takes time -
|
||||||
|
especially if I'm working somewhere with a slow Internet connection and I'm
|
||||||
|
FTP-ing the images into the directory. Also, it's not always the easiest
|
||||||
|
solution for site users - especially non-technical ones.
|
||||||
|
|
||||||
|
So, I wrote the Imagefield Import Archive module. Including comments, the module
|
||||||
|
contains 123 lines, and builds upon the existing functionality of the Imagefield
|
||||||
|
Import module by adding the ability for the user to upload a .zip archive of
|
||||||
|
images. The archive is then moved into the specified import directory and
|
||||||
|
unzipped before being deleted, and the user is directed straight to the standard
|
||||||
|
Imagefield Import page where their images are waiting to be imported, just as
|
||||||
|
usual.
|
||||||
|
|
||||||
|
The module is currently a
|
||||||
|
[sandbox project](http://drupal.org/sandbox/opdavies/1165110) on Drupal.org,
|
||||||
|
although I have applied for full project access so that I can be added as a
|
||||||
|
fully-fledged downloadable module.
|
25
source/_posts/improve-jpg-quality-imagecache-and-imageapi.md
Normal file
25
source/_posts/improve-jpg-quality-imagecache-and-imageapi.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: Improve JPG Quality in Imagecache and ImageAPI
|
||||||
|
date: 2010-06-02
|
||||||
|
excerpt: How to fix the quality of uploaded images in Drupal.
|
||||||
|
tags:
|
||||||
|
- drupal-planet
|
||||||
|
- drupal-6
|
||||||
|
- imagecache
|
||||||
|
---
|
||||||
|
|
||||||
|
Whilst uploading images for my Projects and Testimonials sections, I noticed
|
||||||
|
that the Imagecache-scaled images weren't as high a quality the originals on my
|
||||||
|
Mac. I did some searching online and found out that, by default, Drupal
|
||||||
|
resamples uploaded jpgs to 75% of their original quality.
|
||||||
|
|
||||||
|
To increase the quality of your images, change the setting in the two following
|
||||||
|
places:
|
||||||
|
|
||||||
|
- admin/settings/imageapi/config
|
||||||
|
- admin/settings/image-toolkit
|
||||||
|
|
||||||
|
The first one is for ImageAPI. Primarily, this means Imagecache presets. The
|
||||||
|
second one is for core's image.inc. This is used for resizing profile pictures
|
||||||
|
in core, and some contrib modules. Once changed, I did have to flush each of the
|
||||||
|
Imagecache presets (admin/dist/imagecache) for the changes to take effect.
|
67
source/_posts/include-css-fonts-using-sass-each-loop.md
Normal file
67
source/_posts/include-css-fonts-using-sass-each-loop.md
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
title: Include CSS Fonts by Using a SASS each Loop
|
||||||
|
date: 2014-11-18
|
||||||
|
excerpt: How to use an SASS each loop to easily add multiple fonts to your CSS.
|
||||||
|
tags:
|
||||||
|
- compass
|
||||||
|
- drupal-planet
|
||||||
|
- fonts
|
||||||
|
- sass
|
||||||
|
---
|
||||||
|
|
||||||
|
How to use an @each loop in SASS to quickly include multiple font files within
|
||||||
|
your stylesheet.
|
||||||
|
|
||||||
|
Using a file structure similar to this, organise your font files into
|
||||||
|
directories, using the the font name for both the directory name and for the
|
||||||
|
file names.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
.
|
||||||
|
├── FuturaBold
|
||||||
|
│ ├── FuturaBold.eot
|
||||||
|
│ ├── FuturaBold.svg
|
||||||
|
│ ├── FuturaBold.ttf
|
||||||
|
│ └── FuturaBold.woff
|
||||||
|
├── FuturaBoldItalic
|
||||||
|
│ ├── FuturaBoldItalic.eot
|
||||||
|
│ ├── FuturaBoldItalic.svg
|
||||||
|
│ ├── FuturaBoldItalic.ttf
|
||||||
|
│ └── FuturaBoldItalic.woff
|
||||||
|
├── FuturaBook
|
||||||
|
│ ├── FuturaBook.eot
|
||||||
|
│ ├── FuturaBook.svg
|
||||||
|
│ ├── FuturaBook.ttf
|
||||||
|
│ └── FuturaBook.woff
|
||||||
|
├── FuturaItalic
|
||||||
|
│ ├── FuturaItalic.eot
|
||||||
|
│ ├── FuturaItalic.svg
|
||||||
|
│ ├── FuturaItalic.ttf
|
||||||
|
│ └── FuturaItalic.woff
|
||||||
|
```
|
||||||
|
|
||||||
|
Within your SASS file, start an `@each` loop, listing the names of the fonts. In
|
||||||
|
the same way as PHP's `foreach` loop, each font name will get looped through
|
||||||
|
using the `$family` variable and then compiled into CSS.
|
||||||
|
|
||||||
|
```language-scss
|
||||||
|
@each $family in FuturaBook, FuturaBold, FuturaBoldItalic, FuturaItalic {
|
||||||
|
@font-face {
|
||||||
|
font-family: #{$family};
|
||||||
|
src: url('../fonts/#{$family}/#{$family}.eot');
|
||||||
|
src: url('../fonts/#{$family}/#{$family}.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('../fonts/#{$family}/#{$family}.woff') format('woff'),
|
||||||
|
url('../fonts/#{$family}/#{$family}.ttf') format('truetype'),
|
||||||
|
url('../fonts/#{$family}/#{$family}.svg##{$family}') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the CSS has been compiled, you can then use in your CSS in the standard
|
||||||
|
way.
|
||||||
|
|
||||||
|
```language-scss
|
||||||
|
font-family: "FuturaBook";
|
||||||
|
```
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
title:
|
||||||
|
Include a Local Drupal Settings file for Environment Configuration and
|
||||||
|
Overrides
|
||||||
|
date: 2014-12-20
|
||||||
|
excerpt:
|
||||||
|
How to create and include a local settings file to define and override
|
||||||
|
environment-specific variables.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-6
|
||||||
|
- drupal-7
|
||||||
|
- drupal-8
|
||||||
|
- drupal-planet
|
||||||
|
- settings.php
|
||||||
|
---
|
||||||
|
|
||||||
|
How to create and include a local settings file to define and override
|
||||||
|
environment-specific variables, and keep sensitive things like your database
|
||||||
|
credentials and API keys safe.
|
||||||
|
|
||||||
|
At the bottom of settings.php, add the following code:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$local_settings = __DIR__ . '/settings.local.php';
|
||||||
|
if (file_exists($local_settings)) {
|
||||||
|
include $local_settings;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows for you to create a new file called settings.local.php within a
|
||||||
|
sites/\* directory (the same place as settings.php), and this will be included
|
||||||
|
as an extension of settings.php. You can see the same technique being used
|
||||||
|
within Drupal 8's
|
||||||
|
[default.settings.php](http://cgit.drupalcode.org/drupal/tree/sites/default/default.settings.php#n621)
|
||||||
|
file.
|
||||||
|
|
||||||
|
Environment specific settings like `$databases` and `$base_url` can be placed
|
||||||
|
within the local settings file. Other settings like
|
||||||
|
`$conf['locale_custom_strings_en']` (string overrides) and
|
||||||
|
`$conf['allow_authorize_operations']` that would apply to all environments can
|
||||||
|
still be placed in settings.php.
|
||||||
|
|
||||||
|
settings.php though is ignored by default by Git by a .gitignore file, so it
|
||||||
|
won't show up as a file available to be committed. There are two ways to fix
|
||||||
|
this. The first is to use the `--force` option when adding the file which
|
||||||
|
overrides the ignore file:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
git add --force sites/default/settings.php
|
||||||
|
```
|
||||||
|
|
||||||
|
The other option is to update the .gitignore file itself so that settings.php is
|
||||||
|
no longer ignored. An updated .gitignore file could look like:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
# Ignore configuration files that may contain sensitive information.
|
||||||
|
sites/*/settings.local*.php
|
||||||
|
|
||||||
|
# Ignore paths that contain user-generated content.
|
||||||
|
sites/*/files
|
||||||
|
sites/*/private
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow for settings.php to be added to Git and committed, but not
|
||||||
|
settings.local.php.
|
|
@ -0,0 +1,178 @@
|
||||||
|
---
|
||||||
|
title: How to Install and Configure Subversion (SVN) Server on Ubuntu
|
||||||
|
date: 2011-10-19
|
||||||
|
excerpt: How to install and configure your own SVN server.
|
||||||
|
tags:
|
||||||
|
- svn
|
||||||
|
- ubuntu
|
||||||
|
- version-control
|
||||||
|
---
|
||||||
|
|
||||||
|
Recently, I needed to set up a Subversion (SVN) server on a Ubuntu Linux server.
|
||||||
|
This post is going to outline the steps taken, and the commands used, to install
|
||||||
|
and configure the service.
|
||||||
|
|
||||||
|
Note: As I was using Ubuntu, I was using the 'apt-get' command to download and
|
||||||
|
install the software packages. If you're using a different distribution of
|
||||||
|
Linux, then this command may be different. I'm also assuming that Apache is
|
||||||
|
already installed.
|
||||||
|
|
||||||
|
Firstly, I'm going to ensure that all of my installed packages are up to date,
|
||||||
|
and install any available updates.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, I need to download the subversion, subversion-tools and libapache2
|
||||||
|
packages.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo apt-get install subversion subversion-tools libapache2-svn
|
||||||
|
```
|
||||||
|
|
||||||
|
These are all of the packages that are needed to run a Subversion server.
|
||||||
|
|
||||||
|
## Create subversion directory
|
||||||
|
|
||||||
|
Now, I need to create the directory where my repositories are going to sit. I've
|
||||||
|
chosen this directory as I know that it's one that is accessible to my managed
|
||||||
|
backup service.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo mkdir /home/svn
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a test repository
|
||||||
|
|
||||||
|
First, I'll create a new folder in which I'll create my test project, and then
|
||||||
|
I'll create a repository for it.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo mkdir ~/test
|
||||||
|
$ sudo svnadmin create /home/svn/test -m 'initial project structure'
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a new repository containing the base file structure.
|
||||||
|
|
||||||
|
## Adding files into the test project
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ cd ~/test
|
||||||
|
$ mkdir trunk tags branches
|
||||||
|
```
|
||||||
|
|
||||||
|
I can now import these new directories into the test repository.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo svn import ~/test file:///home/svn/test -m 'Initial project directories'
|
||||||
|
```
|
||||||
|
|
||||||
|
This both adds and commits these new directories into the repository.
|
||||||
|
|
||||||
|
In order for Apache to access the SVN repositories, the `/home/svn` directory
|
||||||
|
needs to be owned by the same user and group that Apache runs as. In Ubuntu,
|
||||||
|
this is usually www-data. To change the owner of a directory, use the chown
|
||||||
|
command.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo chown -R www-data:www-data /home/svn
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring Apache
|
||||||
|
|
||||||
|
The first thing that I need to do is enable the dav_svn Apache module, using the
|
||||||
|
a2enmod command.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ sudo a2enmod dav_svn
|
||||||
|
```
|
||||||
|
|
||||||
|
With this enabled, now I need to modify the Apache configuration file.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ cd /etc/apache2
|
||||||
|
$ sudo nano apache2.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
At the bottom of the file, add the following lines, and then save the file by
|
||||||
|
pressing Ctrl+X.
|
||||||
|
|
||||||
|
```language-apacheconf
|
||||||
|
<Location "/svn">
|
||||||
|
DAV svn
|
||||||
|
SVNParentPath /home/svn
|
||||||
|
</Location>
|
||||||
|
```
|
||||||
|
|
||||||
|
With this saved, restart the Apache service for the changes to be applied.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
sudo service apache2 restart
|
||||||
|
```
|
||||||
|
|
||||||
|
I can now browse through my test repository by opening Firefox, and navigating
|
||||||
|
to `http://127.0.0.1/svn/test`. Here, I can now see my three directories,
|
||||||
|
although they are currently all empty.
|
||||||
|
|
||||||
|
## Securing my SVN repositories
|
||||||
|
|
||||||
|
Before I start committing any files to the test repository, I want to ensure
|
||||||
|
that only authorised users can view it - currently anyone can view the
|
||||||
|
repository and it's contents, as well as being able to checkout and commit
|
||||||
|
files. To do this, I'm going to require the user to enter a username and a
|
||||||
|
password before viewing or performing any actions with the repository.
|
||||||
|
|
||||||
|
Re-open apache2.conf, and replace the SVN Location information with this:
|
||||||
|
|
||||||
|
```language-apacheconf
|
||||||
|
<Location "/svn">
|
||||||
|
DAV svn
|
||||||
|
SVNParentPath /home/svn
|
||||||
|
AuthType Basic
|
||||||
|
AuthName "My SVN Repositories"
|
||||||
|
AuthUserFile /etc/svn-auth
|
||||||
|
Require valid-user
|
||||||
|
</Location>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now I need to create the password file.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ htpasswd -cm /etc/svn-auth oliver
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm prompted to enter and confirm my password, and then my details are saved.
|
||||||
|
The Apache service will need to be restarted again, and then the user will need
|
||||||
|
to authenticate themselves before viewing the repositories.
|
||||||
|
|
||||||
|
## Checking out the repository and commiting files
|
||||||
|
|
||||||
|
For example, now want to checkout the files within my repository into a new
|
||||||
|
directory called 'test2' within my home directory. Firstly, I need to create the
|
||||||
|
new directory, and then I can issue the checkout command.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ cd ~
|
||||||
|
$ mkdir test2
|
||||||
|
$ svn checkout http://127.0.0.1/svn/test/trunk test2
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm passing the command two arguments - the first is the URL of the repository's
|
||||||
|
trunk directory, and the second is the directory where the files are to be
|
||||||
|
placed. As no files have been commited yet into the trunk, it appears to be
|
||||||
|
empty - but if you perform an ls -la command, you'll see that there is a hidden
|
||||||
|
.svn directory.
|
||||||
|
|
||||||
|
Now you can start adding files into the directory. Once you've created your
|
||||||
|
files, perform a svn add command, passing in individual filenames as further
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ svn add index.php
|
||||||
|
$ svn add *
|
||||||
|
```
|
||||||
|
|
||||||
|
With all the required files added, they can be committed using
|
||||||
|
`svn commit -m 'commit message'` command, and the server can be updated using
|
||||||
|
the svn up command.
|
88
source/_posts/install-nomensa-media-player-drupal.md
Normal file
88
source/_posts/install-nomensa-media-player-drupal.md
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
---
|
||||||
|
title: Install and Configure the Nomensa Accessible Media Player in Drupal
|
||||||
|
date: 2012-07-14
|
||||||
|
excerpt:
|
||||||
|
This week I released the first version of the Nomensa Accessible Media Player
|
||||||
|
module for Drupal 7. Here's some instructions of how to install and configure
|
||||||
|
it.
|
||||||
|
tags:
|
||||||
|
- accessibility
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- nomensa
|
||||||
|
---
|
||||||
|
|
||||||
|
This week I released the first version of the Nomensa Accessible Media Player
|
||||||
|
module for Drupal 7. Here's some instructions of how to install and configure
|
||||||
|
it.
|
||||||
|
|
||||||
|
_The official documentation for this module is now located at
|
||||||
|
<https://www.drupal.org/node/2383447>. This post was accurate at the time of
|
||||||
|
writing, whereas the documentation page will be kept up to date with any future
|
||||||
|
changes._
|
||||||
|
|
||||||
|
## Initial configuration
|
||||||
|
|
||||||
|
### Download the Library
|
||||||
|
|
||||||
|
The library can be downloaded directly from GitHub, and should be placed within
|
||||||
|
you _sites/all/libraries/nomensa_amp_ directory.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
drush dl libraries nomensa_amp
|
||||||
|
git clone https://github.com/nomensa/Accessible-Media-Player sites/all/libraries/nomensa_amp
|
||||||
|
cd sites/all/libraries/nomensa_amp
|
||||||
|
rm -rf Accessible-media-player_2.0_documentation.pdf example/ README.md
|
||||||
|
drush en -y nomensa_amp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure the Module
|
||||||
|
|
||||||
|
Configure the module at <em>admin/config/media/nomensa-amp</em> and enable the
|
||||||
|
players that you want to use.
|
||||||
|
|
||||||
|
## Adding videos
|
||||||
|
|
||||||
|
Within your content add links to your videos. For example:
|
||||||
|
|
||||||
|
### YouTube
|
||||||
|
|
||||||
|
```language-html
|
||||||
|
<a href="http://www.youtube.com/watch?v=Zi31YMGmQC4">Checking colour contrast</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vimeo
|
||||||
|
|
||||||
|
```language-html
|
||||||
|
<a href="http://vimeo.com/33729937">Screen readers are strange, when you're a stranger by Leonie Watson</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding captions
|
||||||
|
|
||||||
|
The best way that I can suggest to do this is to use a File field to upload your
|
||||||
|
captions file:
|
||||||
|
|
||||||
|
1. Add a File field to your content type;
|
||||||
|
1. On your page upload the captions file.
|
||||||
|
1. Right-click the uploaded file, copy the link location, and use this for the
|
||||||
|
path to your captions file.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```language-html
|
||||||
|
<a href="http://www.youtube.com/watch?v=Zi31YMGmQC4">Checking colour contrast</a> <a class="captions" href="http://oliverdavies.co.uk/sites/default/files/checking-colour-contrast-captions.xml">Captions for Checking Colour Contrast</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screencast
|
||||||
|
|
||||||
|
<div class="embed-container">
|
||||||
|
<iframe
|
||||||
|
src="https://player.vimeo.com/video/45731954"
|
||||||
|
width="500"
|
||||||
|
height="313"
|
||||||
|
frameborder="0"
|
||||||
|
webkitallowfullscreen
|
||||||
|
mozallowfullscreen
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
15
source/_posts/installing-nagios-centos.md
Normal file
15
source/_posts/installing-nagios-centos.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: Installing Nagios on CentOS
|
||||||
|
date: 2012-04-17
|
||||||
|
excerpt: How to install Nagios on CentOS.
|
||||||
|
tags:
|
||||||
|
- nagios
|
||||||
|
- centos
|
||||||
|
- linux
|
||||||
|
---
|
||||||
|
|
||||||
|
A great post details that details the steps needed to install
|
||||||
|
[Nagios](http://nagios.org) - a popular open source system and network
|
||||||
|
monitoring software application - on CentOS.
|
||||||
|
|
||||||
|
<http://saylinux.net/story/009506/how-install-nagios-centos-55>
|
29
source/_posts/introducing-the-drupal-meetups-twitterbot.md
Normal file
29
source/_posts/introducing-the-drupal-meetups-twitterbot.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
title: Introducing the Drupal Meetups Twitterbot
|
||||||
|
date: 2017-06-09
|
||||||
|
excerpt: I’ve written a twitterbot for promoting Drupal meetups.
|
||||||
|
tags:
|
||||||
|
- twitter
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
<p class="text-center" markdown="1">![](/images/blog/drupal-meetups-twitterbot.png)</p>
|
||||||
|
|
||||||
|
The [Drupal Meetups Twitterbot][0] is a small project that I worked on a few
|
||||||
|
months ago, but hadn't got around to promoting yet. It’s intention is to provide
|
||||||
|
[one Twitter account][1] where people can get the up to date news from various
|
||||||
|
Drupal meetups.
|
||||||
|
|
||||||
|
It works by having a whitelist of [Twitter accounts and hashtags][2] to search
|
||||||
|
for, uses [Codebird][3] to query the Twitter API and retweets any matching
|
||||||
|
tweets on a scheduled basis.
|
||||||
|
|
||||||
|
If you would like your meetup group to be added to the list of searched
|
||||||
|
accounts, please [open an issue][4] on the GitHub repo.
|
||||||
|
|
||||||
|
[0]: https://github.com/opdavies/drupal-meetups-twitterbot
|
||||||
|
[1]: https://twitter.com/drupal_meetups
|
||||||
|
[2]:
|
||||||
|
https://github.com/opdavies/drupal-meetups-twitterbot/blob/master/bootstrap/config.php
|
||||||
|
[3]: https://www.jublo.net/projects/codebird/php
|
||||||
|
[4]: https://github.com/opdavies/drupal-meetups-twitterbot/issues/new
|
34
source/_posts/leaving-nomensa-joining-precedent.md
Normal file
34
source/_posts/leaving-nomensa-joining-precedent.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
title: Leaving Nomensa, Joining Precedent
|
||||||
|
date: 2013-04-20
|
||||||
|
excerpt:
|
||||||
|
Yesterday was my last day working at Nomensa. Next week, I'll be starting as a
|
||||||
|
Senior Developer at Precedent.
|
||||||
|
tags:
|
||||||
|
- nomensa
|
||||||
|
- precedent
|
||||||
|
- personal
|
||||||
|
---
|
||||||
|
|
||||||
|
Yesterday was my last day working at
|
||||||
|
[Nomensa](http://www.nomensa.com 'Nomensa'). Next week, I'll be starting as a
|
||||||
|
Senior Developer at [Precedent](http://www.precedent.co.uk 'Precedent').
|
||||||
|
|
||||||
|
The last 14 months that I've been working at Nomensa have been absolutely
|
||||||
|
fantastic, and had allowed me to work on some great projects for great clients -
|
||||||
|
mainly [unionlearn](http://www.unionlearn.org 'unionlearn') and
|
||||||
|
[Digital Theatre Plus](http://www.digitaltheatreplus.com 'Digital Theatre Plus').
|
||||||
|
I've learned so much about accessibility and web standards, and have pretty much
|
||||||
|
changed my whole approach to front-end development to accommodate best
|
||||||
|
practices. I've also been involved with the Drupal Accessibility group since
|
||||||
|
starting at Nomensa, and have written several accessibility-focused Drupal
|
||||||
|
modules, including the
|
||||||
|
[Nomensa Accessible Media Player](http://drupal.org/project/nomensa_amp 'The Nomensa Accessible Media Player Drupal module')
|
||||||
|
module and the
|
||||||
|
[Accessibility Checklist](http://drupal.org/project/a11y_checklist 'The accessibility checklist for Drupal').
|
||||||
|
I'll definitely be continuing my interest in accessibility, championing best
|
||||||
|
practices, and incorporating it into my future work wherever possible.
|
||||||
|
|
||||||
|
With that all said, I'm really looking forward to starting my new role at
|
||||||
|
Precedent, tackling some new challenges, and I'm sure that it'll be as great a
|
||||||
|
place to work as Nomensa was.
|
713
source/_posts/live-blogging-symfonylive-london.md
Normal file
713
source/_posts/live-blogging-symfonylive-london.md
Normal file
|
@ -0,0 +1,713 @@
|
||||||
|
---
|
||||||
|
title: Live Blogging From SymfonyLive London 2019
|
||||||
|
date: 2019-09-13
|
||||||
|
tags:
|
||||||
|
- conference
|
||||||
|
- symfony
|
||||||
|
- symfonylive
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
Inspired by [Matt Stauffer](https://twitter.com/stauffermatt)'s
|
||||||
|
[live blogging of the keynote](https://mattstauffer.com/blog/introducing-laravel-vapor)
|
||||||
|
at Laracon US, I’m going to do the same for the sessions that I’m attending at
|
||||||
|
[SymfonyLive London 2019](https://london2019.live.symfony.com)...
|
||||||
|
|
||||||
|
## Keynote (Back to the basics)
|
||||||
|
|
||||||
|
**Embrace the Linux philosophy**
|
||||||
|
|
||||||
|
- How we grow the Symfony ecosystem. Built abstracts.
|
||||||
|
- HttpFoundation, HttpKernel
|
||||||
|
- Moved to infrastructure
|
||||||
|
- A few abstractions on top of PHP. Improved versions of PHP functions (`dump`
|
||||||
|
and `var_dump`)
|
||||||
|
- Started a add higher level abstractions (e.g. Mailer), built on the lower
|
||||||
|
ones.
|
||||||
|
- Recently worked on PHPUnit assertions. Mailer in Symony 4.4. Can test if an
|
||||||
|
email is sent or queued
|
||||||
|
|
||||||
|
**Building flexible high-level abstractions on top of low-level ones**
|
||||||
|
|
||||||
|
### What's next?
|
||||||
|
|
||||||
|
- Mailer announced in London last year. New component.
|
||||||
|
- System emails? e.g. new customer, new invoice.
|
||||||
|
- Symfony Mailer = Built-in responsive, flexible, and generic system emails
|
||||||
|
- Twig with TwigExtraBundle
|
||||||
|
- Twig `inky-extra` package (Twig 1.12+)
|
||||||
|
- Zurb Foundation for Emails CSS stylesheet
|
||||||
|
- Twig `cssinliner-extra` package (Twig 1.12+)
|
||||||
|
- Optimised Twig layouts
|
||||||
|
- `SystemEmail` class extends templated email
|
||||||
|
- Can set importance,
|
||||||
|
- Customisable
|
||||||
|
- Always trying to keep flexible, so things can be overidden and customised
|
||||||
|
|
||||||
|
### Sending SMS messages
|
||||||
|
|
||||||
|
- new `Texter` and `SmsMessage` class for sending SMS messages
|
||||||
|
- Same abstraction as emails, but for SMS messages
|
||||||
|
- Based on HttpClient + Symfony Messenger and third-party providers (Twilio and
|
||||||
|
Nexmo) `twilio://` and `nemxo://`
|
||||||
|
- Can set via transport `$sms->setTransport('nexmo')`
|
||||||
|
- Extend the `SystemEmail` and do what you want
|
||||||
|
- Failover
|
||||||
|
|
||||||
|
### Sending Messages
|
||||||
|
|
||||||
|
- Create `ChatMessage`
|
||||||
|
- Telegram and Slack
|
||||||
|
- `$message->setTransport('telegram')`, `$bus->dispatch($message)`
|
||||||
|
- Send to Slack **and** Telegram
|
||||||
|
- `SlackOptions` and `TelegramOptions` for adding emojis etc
|
||||||
|
- Common transport layer `TransportInterface`, `MessageInterface`
|
||||||
|
- Failover - e.g. if Twilio is down, send to Telegram
|
||||||
|
|
||||||
|
### New component - SymfonyNotifier
|
||||||
|
|
||||||
|
- Channels - email, SMS, chat
|
||||||
|
- Transport, slack, telegram, twilio
|
||||||
|
- Create a notification, arguments are message and transports (array)
|
||||||
|
- Receiver
|
||||||
|
- Customise notifications, `InvoiceNotification` extends `Notification`.
|
||||||
|
`getChannels`
|
||||||
|
- Override default rendering
|
||||||
|
- `ChatNotificationInterface` - `asChatMessage()`
|
||||||
|
- Semantic configuration
|
||||||
|
- `composer req twilio-notifier telegram-notifier`
|
||||||
|
- Channels
|
||||||
|
- Mailer
|
||||||
|
- Chatter
|
||||||
|
- Texter
|
||||||
|
- Browser
|
||||||
|
- Pusher (iOS, Android, Desktop native notifications)
|
||||||
|
- Database (web notification centre)
|
||||||
|
- **A unified way to notify Users via a unified Transport layer**
|
||||||
|
- Each integration is only 40 lines of code
|
||||||
|
|
||||||
|
### What about a SystemNotification?
|
||||||
|
|
||||||
|
- Autoconfigured channels
|
||||||
|
- `new SystemNotification`, `Notifier::getSystemReceivers`
|
||||||
|
- Importance, automatically configures channels
|
||||||
|
- Different channels based on importance
|
||||||
|
- `ExceptionNotification` - get email with stack trace attached
|
||||||
|
|
||||||
|
Notifier
|
||||||
|
|
||||||
|
- send messages via a unified api
|
||||||
|
- send to one or many receivers
|
||||||
|
- Default configu or custom one
|
||||||
|
|
||||||
|
### How can we leverage this new infrastructure?
|
||||||
|
|
||||||
|
- `Monolog NotifierHandler` - triggered on `Error` level logs
|
||||||
|
- Uses notified channel configuration
|
||||||
|
- Converts Error level logs to importance levels
|
||||||
|
- Configurablelike other Notifications
|
||||||
|
- 40 lines of code
|
||||||
|
- Failed Messages Listener - 10 lines of glue code
|
||||||
|
|
||||||
|
- **Experimental component in 5.0**
|
||||||
|
- Can't in in 4.4 as it's a LTS version
|
||||||
|
- First time an experimental component is added
|
||||||
|
- Stable in 5.1
|
||||||
|
|
||||||
|
## Queues, busses and the Messenger component (Tobias Nyholm)
|
||||||
|
|
||||||
|
- Stack is top and buttom - Last-in, first-out
|
||||||
|
- Queue is back and front - last in, first out
|
||||||
|
|
||||||
|
### 2013
|
||||||
|
|
||||||
|
- Using Symfony, used 40 or 50 bundles in a project - too much information!
|
||||||
|
- Used to copy and paste, duplicate a lot of code
|
||||||
|
- Testing your controllers - controllers as services?
|
||||||
|
- Controllers are 'comfortable'
|
||||||
|
- Tried adding `CurrentUserProvider` service to core, should be passed as an
|
||||||
|
argument. Cannot test.
|
||||||
|
- 'Having Symfony all over the place wasn't the best thing' - when to framework
|
||||||
|
(Matthias Noback)
|
||||||
|
- Hexagonal architecture
|
||||||
|
- Keep your kernel away from infrastructure. Let the framework handle the
|
||||||
|
infrastructure.
|
||||||
|
- Controller -> Command -> Command Bus -> `CommandHandler`
|
||||||
|
|
||||||
|
#### What did we win?
|
||||||
|
|
||||||
|
- Can leverage Middleware with a command bus
|
||||||
|
- Queues as a service (RabbitMQ)
|
||||||
|
- Work queue - one producer, multiple consumers
|
||||||
|
- Queues should be durable - messages are also stored on disk, consumers should
|
||||||
|
acknowledge a message once a message is handled
|
||||||
|
- Publish/subscribe
|
||||||
|
- Producer -> Fanout/direct with routing (multiple queues) -> multiple
|
||||||
|
consumers
|
||||||
|
- Topics - wildcards
|
||||||
|
|
||||||
|
### 2016
|
||||||
|
|
||||||
|
- New intern. Understand everything, 'just PHP'. Plain PHP application, not
|
||||||
|
'scary Symfony'
|
||||||
|
|
||||||
|
### Symfony Messenger
|
||||||
|
|
||||||
|
- `composer req symfony/messager` - best MessageBus implementation
|
||||||
|
- Message -> Message bus -> Message handler
|
||||||
|
- Message is a plain PHP class
|
||||||
|
- Handler is a normal PHP class which is invokable
|
||||||
|
- `messenger:message_hander` tag in config
|
||||||
|
- Autowire with `MessageHandlerInterface`
|
||||||
|
- What if it takes 20 seconds to send a message? Use asynchronous.
|
||||||
|
- Transports as middleware (needs sender, receiver, configurable with DSN,
|
||||||
|
encode/decode). `MESSENGER_DSN` added to `.env`
|
||||||
|
- Start consumer with `bin/console messager:consume-messages`. Time limit with
|
||||||
|
`--time-limit 300`
|
||||||
|
- PHP Enqueue - production ready, battle-tested messaging solution for PHP
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
- Transformers, takes an object and transforms into an array -
|
||||||
|
`FooTransformer implements TransformerInterface`.
|
||||||
|
- Don't break other apps by changing the payload.
|
||||||
|
|
||||||
|
#### Multiple buses
|
||||||
|
|
||||||
|
- Command bus, query bus, event bus
|
||||||
|
- Separate actions from reactions
|
||||||
|
|
||||||
|
#### Envelope
|
||||||
|
|
||||||
|
- Stamps for metadata - has the item been on the queue already?
|
||||||
|
|
||||||
|
#### Failures
|
||||||
|
|
||||||
|
- Requeue, different queue or same queue after a period of time
|
||||||
|
- Failed queue 1 every minute, failed queue 2 every hour - temporary glitches or
|
||||||
|
a bug?
|
||||||
|
|
||||||
|
#### Creating entities
|
||||||
|
|
||||||
|
- What if two users registered at the same tiem? Use uuids rather than IDs.
|
||||||
|
- Symfony validation - can be used on messages, not just forms.
|
||||||
|
|
||||||
|
- Cache everything
|
||||||
|
|
||||||
|
- Option 1: HTTP request -> Thin app (gets responses from Redis) -> POST to
|
||||||
|
queue. Every GET request would warm cache
|
||||||
|
- Option 2: HTTP request -> Thin app -> return 200 response -> pass to workers
|
||||||
|
|
||||||
|
- Tip: put Command and CommandHandlers in the same directory
|
||||||
|
|
||||||
|
## HttpClient (Nicolas Grekas)
|
||||||
|
|
||||||
|
- new symfony component, released in may
|
||||||
|
- Httpclient contracts, separate package that contains interfaces
|
||||||
|
- Symfony
|
||||||
|
- PHP-FIG
|
||||||
|
- Httplug
|
||||||
|
- `HttpClient::create()`. `$client->get()`
|
||||||
|
- JSON decoded with error handling
|
||||||
|
- Used on symfony.com website (#1391). Replaces Guzzle `Client` for
|
||||||
|
`HttpClientInterface`
|
||||||
|
- Object is stateless, Guzzle is not. Doesn't handle cookies, cookies are state
|
||||||
|
- Remove boilerplate - use `toArray()`
|
||||||
|
- Options as third argument - array of headers, similar to Guzzle
|
||||||
|
|
||||||
|
### What can we do with the Response?
|
||||||
|
|
||||||
|
- `getStatusCode(): int`
|
||||||
|
- `getHeaders(): array`
|
||||||
|
- `getContent(): string`
|
||||||
|
- `toArray(): array`
|
||||||
|
- `cancel(): void`
|
||||||
|
- `getInfo(): array` - metadata
|
||||||
|
- Everything is lazy!
|
||||||
|
- 80% of use-cases covered
|
||||||
|
|
||||||
|
### What about PSR-18?
|
||||||
|
|
||||||
|
- Decorator/adapter to change to PSR compatible
|
||||||
|
- Same for Httplug
|
||||||
|
|
||||||
|
### What about the remaining 20%?
|
||||||
|
|
||||||
|
- Options are part of the abstraction, not the implementation
|
||||||
|
|
||||||
|
#### Some of the options
|
||||||
|
|
||||||
|
- `timeout` - control inactivity periods
|
||||||
|
- `proxy` - get through a http proxy
|
||||||
|
- `on_progress` - display a progress bar / build a scoped client
|
||||||
|
- `base_url` - resolve relative URLS / build a scoped client
|
||||||
|
- `resolve` - protect webhooks against calls to internal endpoints
|
||||||
|
- `max_redirects` - disable or limit redirects
|
||||||
|
|
||||||
|
- Robust and failsafe by default
|
||||||
|
|
||||||
|
- Streamable uploads - `$mimeParts->toIterable()`.
|
||||||
|
- donwload a file
|
||||||
|
|
||||||
|
```php
|
||||||
|
foreach ($client->stream($response) as $chunk) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Responses are lazy, requests are concurrent
|
||||||
|
* Asychronus requests. Reading in network order
|
||||||
|
|
||||||
|
```
|
||||||
|
foreach ($client->stream($responses) as $response => $chunk) {
|
||||||
|
if ($chunk->isLast()) {
|
||||||
|
// a $response completed
|
||||||
|
} else {
|
||||||
|
// a $response's got network activity or timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 379 request completed in 0.4s!
|
||||||
|
- `Stream` has second argument, max number of seconds to wait before yielding a
|
||||||
|
timeout chunk
|
||||||
|
- `ResponseInterface::getInfo()` - get response headers, redirect count and URL,
|
||||||
|
start time, HTTP method and code, user data and URL
|
||||||
|
- `getInfo('debug')` - displays debug information
|
||||||
|
|
||||||
|
### The components
|
||||||
|
|
||||||
|
- `NativeHttpClient` and `CurlHttpClient`
|
||||||
|
- both provide
|
||||||
|
- 100% contracts
|
||||||
|
- secure directs
|
||||||
|
- extended (time) info
|
||||||
|
- transparent HTTP compression and (de)chunking
|
||||||
|
- automatic HTTP proxy configuration via env vars
|
||||||
|
|
||||||
|
#### `NativeHttpClient`
|
||||||
|
|
||||||
|
- is most portable, works for everyone
|
||||||
|
- based on HTTP stream wrapper with fixed redirect logic
|
||||||
|
- blocking until response headers arrive
|
||||||
|
|
||||||
|
#### `CurlHttpClient`
|
||||||
|
|
||||||
|
- Requires ext-curl with fixed redirection logic
|
||||||
|
- Multiplexing response headers and bodies
|
||||||
|
- Leverages HTTP/2 and PUSH when available
|
||||||
|
- Keeps connections open also between synchronous requests, no DNS resolution so
|
||||||
|
things are faster
|
||||||
|
|
||||||
|
#### Decorators
|
||||||
|
|
||||||
|
- ScopingHttpClient - auto-configure options based on request URL
|
||||||
|
- MockHttpClient - for testing, doesn't make actual HTTP requests
|
||||||
|
- CachingHttpClient - adds caching on a HTTP request
|
||||||
|
- Psr18Client
|
||||||
|
- HttplugClient
|
||||||
|
- TraceableHttpClient
|
||||||
|
|
||||||
|
### Combining
|
||||||
|
|
||||||
|
#### FrameworkBundle/Autowiring
|
||||||
|
|
||||||
|
```yml
|
||||||
|
framework:
|
||||||
|
http_client:
|
||||||
|
max_host_connections: 4
|
||||||
|
deault_options:
|
||||||
|
# ....
|
||||||
|
scoped_client:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HttpBrowser
|
||||||
|
|
||||||
|
- HttpClient + DomCrawler + CssSelector + HttpKernel + BrowserKit
|
||||||
|
- RIP Goutte!
|
||||||
|
|
||||||
|
### Coming in 4.4...
|
||||||
|
|
||||||
|
- `max_duration`
|
||||||
|
- `buffer` based on a callable
|
||||||
|
- `$chunk->isInformational()`
|
||||||
|
- `$response->toStream()`
|
||||||
|
- Async-compatible extensibility, when decoration is not enough
|
||||||
|
|
||||||
|
`composer req symfony/http-client`
|
||||||
|
|
||||||
|
## Symfony Checker is coming (Valentine Boineau)
|
||||||
|
|
||||||
|
- Static analysis tool for Symfony
|
||||||
|
- Does a method exist?
|
||||||
|
- Is it deprecated?
|
||||||
|
- insight.symfony.com
|
||||||
|
- @symfonyinsight
|
||||||
|
- Released soon
|
||||||
|
|
||||||
|
### Differences
|
||||||
|
|
||||||
|
- Specialise in Symfony - can see more relevant things
|
||||||
|
- Different interface to other services
|
||||||
|
|
||||||
|
## Feeling unfulfilled by SPA promises? Go back to Twig (Dan Blows)
|
||||||
|
|
||||||
|
A way on the front-end JS, CSS, images at the beginning of the request, sends a
|
||||||
|
HTTP request (XHR/AJAX) to the back-end
|
||||||
|
|
||||||
|
### Why SPAs?
|
||||||
|
|
||||||
|
- A way on the front-end JS, CSS, images at the beginning of the request, sends
|
||||||
|
a HTTP request (XHR/AJAX) to the back-end
|
||||||
|
- no full page refresh
|
||||||
|
- Supposed to be much quicker
|
||||||
|
- 'Right tool for the job' - JS on the front-end, PHP on the back-end
|
||||||
|
- Division of responsibility == faster development
|
||||||
|
- Reusable API - Api -> Mobile App and SPA - easy to add another consumer
|
||||||
|
- Easier to debug?
|
||||||
|
|
||||||
|
### Why not SPAs?
|
||||||
|
|
||||||
|
- Lots of HTTP requests (400 to load the initial page on one project) == slow
|
||||||
|
front end
|
||||||
|
- Blurred responsibilities == tightly coupled teams
|
||||||
|
- harder to debug, bugs fall between systems and teams. Huge gap between
|
||||||
|
front-end and back-end, passing responsibilites.
|
||||||
|
- You can fix these problems in SPAs, but is it worth it?
|
||||||
|
- Examples of good SPAs - Trello, Flickr
|
||||||
|
|
||||||
|
### Using Twig as an alternative to an SPA?
|
||||||
|
|
||||||
|
#### Faster UI - Try and figure out where the problem is.
|
||||||
|
|
||||||
|
If you're trying to speed things up, find out where the problem is.
|
||||||
|
|
||||||
|
- Browser tools
|
||||||
|
- Web Debug Toolbar
|
||||||
|
- Blackfire
|
||||||
|
- Optimise and monitor
|
||||||
|
|
||||||
|
#### Speed up Twig
|
||||||
|
|
||||||
|
- Speeding up Symfony
|
||||||
|
- ext/twig (PHP5 only, not PHP 7)
|
||||||
|
- Store compiled templates in Opcache, make sure it's enabled
|
||||||
|
- Render assets though the webserver (assetic not running all the time)
|
||||||
|
|
||||||
|
#### Edge side includes
|
||||||
|
|
||||||
|
- Component cached differently to the rest of the page
|
||||||
|
- Varnish/Nginx
|
||||||
|
- `render_esi`
|
||||||
|
- News block that caches frequently, rest of the page
|
||||||
|
|
||||||
|
#### HTTP/2 with Weblink
|
||||||
|
|
||||||
|
- slow finding CSS files to load - 'push' over CSS files, doesn't need to wait
|
||||||
|
- `preload()` - https://symfony.com/doc/current/web_link.html
|
||||||
|
|
||||||
|
#### Live updating pages
|
||||||
|
|
||||||
|
- Instantly update when sports results are updated, news articles are added
|
||||||
|
- Mercure - https://github.com/symfony/mercure
|
||||||
|
- LiveTwig - whole block or whole section, and live update `render_live`
|
||||||
|
- Turbolinks - replace whole body, keeps CSS and JS in memory. Merges new stuff
|
||||||
|
in. `helthe/turbolinks`
|
||||||
|
- ReactPHP - shares kernel between requests
|
||||||
|
|
||||||
|
### Writing better code with Twig
|
||||||
|
|
||||||
|
- Keep templates simple. Avoid spaghetti code, only about UI. HTML or small
|
||||||
|
amounts of Twig.
|
||||||
|
- Avoid delimeter chains
|
||||||
|
- Bad:`blog_post.authors.first.user_account.email_address`
|
||||||
|
- Good `{{ blog_post.authors_email_address }}`
|
||||||
|
- Less brittle, slow
|
||||||
|
|
||||||
|
* Filters
|
||||||
|
- Use filters to be precise
|
||||||
|
- Custom filters
|
||||||
|
- Avoid chains. Can cause odd results. Create a new filter in PHP
|
||||||
|
* Functions
|
||||||
|
- Write your own functions
|
||||||
|
- Simpler templates
|
||||||
|
- Get data, can use boolean statements
|
||||||
|
* Components
|
||||||
|
- Break a page into components rather than one large page
|
||||||
|
- `include()`
|
||||||
|
- Use `only` to only pass that data. less tightenly coupled.
|
||||||
|
* `render` calls the whole of Symfony, boots Kernel, can be expensive and slow
|
||||||
|
* Loosely couple templates and controllers
|
||||||
|
- Keep responses simple
|
||||||
|
- What makes sense
|
||||||
|
- if you need extra data in the template, get it in the template
|
||||||
|
* View models
|
||||||
|
- Mixed results
|
||||||
|
- `BlogPostViewModel`
|
||||||
|
- Can result in boilerplate code
|
||||||
|
- Can be useful if the view model is different to the Entity
|
||||||
|
* DRY
|
||||||
|
- "Don't repeat yourself"
|
||||||
|
|
||||||
|
- Faster development
|
||||||
|
- Separate UI tests from back-end tests. Different layers for different teams.
|
||||||
|
People don't need to run everything if they are only changing certain
|
||||||
|
things.
|
||||||
|
|
||||||
|
* Help your front end
|
||||||
|
- Webpack - Encore
|
||||||
|
- Type hinting in functions and filters, easier to debug
|
||||||
|
- Logging
|
||||||
|
- Friendly exceptions - help front-end devs by returning meaningful, readbale
|
||||||
|
errors
|
||||||
|
- Web Debug Toolbar and Profiler, provide training for toolbar and profilers
|
||||||
|
- Twig-friendly development environment - Twig support in IDEs and text
|
||||||
|
editors
|
||||||
|
|
||||||
|
SPAs are sometimes teh right solution. Why do they want to use it, can the same
|
||||||
|
benefits be added with Twig?
|
||||||
|
|
||||||
|
3 most important points:
|
||||||
|
|
||||||
|
- Profile, identidy, optimise, monitor
|
||||||
|
- Loosely couple templates to your app code
|
||||||
|
- Help your front ends - put your front end developers first
|
||||||
|
- You don't need to use a SPA for single pages, use JavaScript for that one
|
||||||
|
page. It doesn't need to be all or nothing.
|
||||||
|
|
||||||
|
## BDD Your Symfony Application (Kamil Kokot)
|
||||||
|
|
||||||
|
- Applying BDD to Sylius
|
||||||
|
- 2 years since release of Sylius (Symfony 2 alpha)
|
||||||
|
- The business part is more important than the code part
|
||||||
|
|
||||||
|
### What is BDD?
|
||||||
|
|
||||||
|
- Behaviour driven development. Combines TDD and DDD, into an agile methodology
|
||||||
|
- Encourages communication and creates shared understanding
|
||||||
|
- Living, executable documentation that non-programmers understand. Always
|
||||||
|
correct.
|
||||||
|
- Feature file
|
||||||
|
- Feature
|
||||||
|
- Scenario - example of the behaviour for this feature. Simple, atomic. (e.g.
|
||||||
|
I need a product in order to add it to a cart)
|
||||||
|
- In order to...
|
||||||
|
- Who gets the benefit?
|
||||||
|
|
||||||
|
### BDD in practice
|
||||||
|
|
||||||
|
- Feature: booking flight tickets
|
||||||
|
- Scenario: booking flight ticket for one person
|
||||||
|
- Given there are the following flights...
|
||||||
|
- When I visit '/flight/LTN-WAW'
|
||||||
|
- Then I should be on '/flight/LTN-WAW'
|
||||||
|
- Add I should see "Your flight has been booked." in "#result"
|
||||||
|
- In the BDD way - what is the business logic? What is the value for this
|
||||||
|
scenario? What is the reason 'why', and who benefits from this?
|
||||||
|
- We just need to know that there are 5 seats left on a flight
|
||||||
|
- Talk and communicate about how the feature is going to work - not just
|
||||||
|
developers
|
||||||
|
- BDD aids communication
|
||||||
|
- Questions we can ask
|
||||||
|
- Can we get a different outcome when the context changes?
|
||||||
|
- When there was only one seat available
|
||||||
|
- When there were no available seats
|
||||||
|
- Can we get the same outcome when the event changes? Can we change 'When' and
|
||||||
|
'Then stays the same'
|
||||||
|
- When it is booked for an adult and a child
|
||||||
|
- When it is booked for an adult
|
||||||
|
- Does anything else happen that is not mentioned?
|
||||||
|
- Generate an invoice if a seat is booked
|
||||||
|
- a pilot would like to get a notification that a seat was booked.
|
||||||
|
* Figuring out the rules
|
||||||
|
- Adults are 15+ years old
|
||||||
|
- Children are 2-14 years old
|
||||||
|
- Infants and children can only travel with an adult
|
||||||
|
- We don't allow for overbooking
|
||||||
|
- Translating rules into examples
|
||||||
|
- Add a new scenario for each rule - e.g. don't allow over booking
|
||||||
|
- "And the flight should be no longer available..."
|
||||||
|
|
||||||
|
### Behat
|
||||||
|
|
||||||
|
- Used to automate and execute BDD tests, also SpecDDD
|
||||||
|
- maps steps to PHP code
|
||||||
|
- Given a context, when an event, then an outcome
|
||||||
|
- Domain Context, API context
|
||||||
|
- class implements `Context`, annotations for `@Given`, `@When`, `@Then`. allows
|
||||||
|
for arguments and regular expressions
|
||||||
|
- Suites: change what code is executed, and what scenarios are executed. context
|
||||||
|
and tags
|
||||||
|
- FriendsOfBehat SymfonyExtension - integrates Behat with Symfony
|
||||||
|
- Contexts registered as Symfony services - inject dependencies, service as a
|
||||||
|
context in Behat. Need to be 'public' for it to work
|
||||||
|
- Reduces boilerplate code. Supports autowiring.
|
||||||
|
- Zero configuration
|
||||||
|
|
||||||
|
### Domain context
|
||||||
|
|
||||||
|
- `Given` verb matches `@Given` annotation. Same for `When` and `Then`.
|
||||||
|
- Transformers, type hint name string, return Client instance
|
||||||
|
|
||||||
|
### API context
|
||||||
|
|
||||||
|
- inject `FlightBookingService` and `KernelBrowser`
|
||||||
|
- Use `$this->kernelBrowser->request()`
|
||||||
|
- Use `assert()` function wuthin `@Then`
|
||||||
|
|
||||||
|
### Back to reality - how it's done with Sylius
|
||||||
|
|
||||||
|
- Business part applies to all context. Start talking about what needs to be
|
||||||
|
done, start communicating
|
||||||
|
- Implement contexts for UI and API
|
||||||
|
- 12716 steps, 1175 scenarios, 8 min 8 sec, 2.4 scenarios /sec
|
||||||
|
- 12x faster than JS (17 min 48 sec, 0.19 scenario / sec)
|
||||||
|
- Treat test CI environment like production
|
||||||
|
|
||||||
|
- Turn off debug settings, add caching
|
||||||
|
- Enable OPcache
|
||||||
|
|
||||||
|
- Write features in a natural way
|
||||||
|
- Too many setup steps - merge steps. less visual debt. e.g. Create currency,
|
||||||
|
zone and locale when creating a store
|
||||||
|
- Avoid scenarios that are too detailed. You should specify only what's
|
||||||
|
important to this scenario.
|
||||||
|
|
||||||
|
## Migrating to Symfony one route at a time (Steve Winter)
|
||||||
|
|
||||||
|
- New client with an old application, built in an old version of another
|
||||||
|
framework with unusual dependency management, no tests, no version control and
|
||||||
|
deploying via FTP. Done over a ~3 month period.
|
||||||
|
|
||||||
|
- Subscription based index of suppliers
|
||||||
|
- New requirements to implement by the client
|
||||||
|
- Our requirements: Needed a deployment process, make it testable, fix the build
|
||||||
|
chain
|
||||||
|
- Solution attempt 1: Migrate to a new version of the current framework
|
||||||
|
- Minor template and design changes were fine
|
||||||
|
- Modifiy features, add new dependencies.
|
||||||
|
- Solution attempt 2: Upgrade to the latest version - same outcome due to
|
||||||
|
multiple BC breaks (no semver), lots of manual steps
|
||||||
|
- Solution attempt 3: Symfony!
|
||||||
|
- Semver! Backwards compatibility promise
|
||||||
|
- Symfony app to run in parallel, Apache proxy rules and minor changes to the
|
||||||
|
legacy app, added data transfer mechanisms
|
||||||
|
- Anything new done in Symfony
|
||||||
|
- Installed on the same server with it's own vhost but not publicly accessible
|
||||||
|
- Deployed independently of legacy app
|
||||||
|
|
||||||
|
### Apache proxy rules
|
||||||
|
|
||||||
|
Proxy `/public` to symfony app
|
||||||
|
|
||||||
|
### Legacy app
|
||||||
|
|
||||||
|
- Shared cookie for single login between apps - user account details (name etc),
|
||||||
|
session details (login time)
|
||||||
|
|
||||||
|
### Added functionality
|
||||||
|
|
||||||
|
- Built in Symfony
|
||||||
|
- new proxy rules for new routes
|
||||||
|
- Add menu links to legacy app menu
|
||||||
|
- How do we show how many reminders are active?
|
||||||
|
- Symfony based API called from the front-end
|
||||||
|
|
||||||
|
### Migrating routes
|
||||||
|
|
||||||
|
- Rebuilt or extend in Symfony app
|
||||||
|
- Test and deploy, then update the apache config to add new proxy rules
|
||||||
|
|
||||||
|
### A gotcha
|
||||||
|
|
||||||
|
- Legacy app uses CSRF
|
||||||
|
- Needed to track the token, added to shared cookie and pass through to the
|
||||||
|
Symfony side
|
||||||
|
|
||||||
|
### Storing data
|
||||||
|
|
||||||
|
- Both apps using the same data with different credentials
|
||||||
|
- Some shared tables, some tables are specific to each app
|
||||||
|
|
||||||
|
### Remaining challenges
|
||||||
|
|
||||||
|
- User session management, still handled by legacy app
|
||||||
|
- Templating/CSS - two versions of everything
|
||||||
|
- Next step: move all CSS to Symfony
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
- Add Symfony app, Apache proxy rules for routes
|
||||||
|
- User transfer mechanisms
|
||||||
|
- New functionality added in Symfony
|
||||||
|
|
||||||
|
### Is this right for you?
|
||||||
|
|
||||||
|
It depends. Fine for a 'modest' size. Use a real proxy for larger scale apps,
|
||||||
|
use different servers with database replication.
|
||||||
|
|
||||||
|
## Closing Keynote: The fabulous World of Emojis and other Unicode symbols (Nicolas Grekas)
|
||||||
|
|
||||||
|
- ASCII. Still used today. Map between the first 128 numbers to characters. OK
|
||||||
|
for UK and US.
|
||||||
|
- 256 numbers in Windows-1252 (character sets). Each country had their own set.
|
||||||
|
- It's legacy. 0.2% for Windows-1252. 88.8% for UTF-8 (Feb 2017)
|
||||||
|
- Unicode: 130k characters, 135 scripts (alphabets)
|
||||||
|
- Validation errors using native alphabet - e.g. invalid last name when
|
||||||
|
submitting a form
|
||||||
|
- 17 plans, each square is 255 code points
|
||||||
|
- Emojis are characters, not images
|
||||||
|
- Gliph is a visual representation of a character
|
||||||
|
- From code points to bytes
|
||||||
|
- UTF-8: 1,2,3 or 4 bytes
|
||||||
|
- UTF16: 2 or 4 bytes
|
||||||
|
- UTF-32: 4 bytes
|
||||||
|
- UTF-8 is compatible with ASCII
|
||||||
|
- Case sensitivity - 1k characters are concerned. One uppercase letter, two
|
||||||
|
lower case variants. Turkish exception (similar looking letters that are
|
||||||
|
different letters with different meanings). Full case folding.
|
||||||
|
- Collations - ordering is depends on the language. 'ch' in Spanish is a single
|
||||||
|
character.
|
||||||
|
- Single number in unicode to represent accents. Combining characters.
|
||||||
|
- Composed (NFC) and decomposed (NFD) forms - normalisation for comparison
|
||||||
|
- Grapheme clusters - multiple characters, but one letter as you write it
|
||||||
|
(separate characters for letters and accent)
|
||||||
|
- Emjois - combining characters. e.g. Combine face with colour. Different codes
|
||||||
|
and character names. Also applies to ligatures. A way to combine several
|
||||||
|
images together into one single visual representation.
|
||||||
|
|
||||||
|
### unicode fundamentals
|
||||||
|
|
||||||
|
- uppercase, lowercase, folding
|
||||||
|
- compositions, ligatures
|
||||||
|
- comparistions - normalisations and collations
|
||||||
|
- segmentation: characters, words, sentences and hyphens
|
||||||
|
- locales: cultural conventions, translitterations
|
||||||
|
- identifiers & security, confusables
|
||||||
|
- display: direction, width
|
||||||
|
|
||||||
|
### unicode in practice
|
||||||
|
|
||||||
|
- MySQL - `utf*_*`. `SET NAMES utf8mb4` for security and storing emojis. Cannot
|
||||||
|
store emojis with `utf8`
|
||||||
|
|
||||||
|
### in php
|
||||||
|
|
||||||
|
- `mb_*()`
|
||||||
|
- `iconv_*()`
|
||||||
|
- `preg_*()`
|
||||||
|
- `grapheme_*()` `normalizer_*()`
|
||||||
|
- `symfony/polyfill-*` - pure PHP implementation
|
||||||
|
- Made a component - **symfony/string** -
|
||||||
|
https://github.com/symfony/symfony/pull/33553
|
||||||
|
- Object orientated api for strings. Immutable value objects
|
||||||
|
- `AbstractString`
|
||||||
|
- `GraphemeString`
|
||||||
|
- `Utf8String`
|
||||||
|
- `BinaryString`
|
||||||
|
|
||||||
|
* AbstractString - Methods to serialize, get length, to binary or grapheme or
|
||||||
|
utf8
|
||||||
|
- Methods for starts with, ends with, is empty, join, prepend, split, trim,
|
||||||
|
title etc
|
68
source/_posts/looking-forward-to-drupalcamp-london.md
Normal file
68
source/_posts/looking-forward-to-drupalcamp-london.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
title: Looking forward to DrupalCamp London
|
||||||
|
date: 2018-02-27
|
||||||
|
excerpt:
|
||||||
|
This weekend is DrupalCamp London 2018. I’ll be there along with a number of
|
||||||
|
my Microserve colleagues.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupalcamp
|
||||||
|
- drupalcamp-london
|
||||||
|
- speaking
|
||||||
|
---
|
||||||
|
|
||||||
|
This weekend is [DrupalCamp London 2018][1]. I’ll be there along with a number
|
||||||
|
of my [Microserve][2] colleagues.
|
||||||
|
|
||||||
|
I look forward to DrupalCamp London every year, partly because it was the first
|
||||||
|
DrupalCamp that I attended back in 2014. It was also the first DrupalCamp that I
|
||||||
|
[gave a talk][3] at, when I presented a session about Git Flow having given only
|
||||||
|
one user group talk before.
|
||||||
|
|
||||||
|
I’ve presented sessions at every DrupalCamp London since (including two last
|
||||||
|
year), and I’m lucky enough to be [speaking again this year][4] due to one of
|
||||||
|
the originally announced speakers no longer being able to make it to the event.
|
||||||
|
|
||||||
|
Here are some other sessions that I’m hoping to see (in no particular order):
|
||||||
|
|
||||||
|
- Keynote by [Ryan Szrama][5] from [Commerce Guys][6]
|
||||||
|
- [Drupal 8 Services And Dependency Injection](https://drupalcamp.london/session/drupal-8-services-and-dependency-injection)
|
||||||
|
by Phil Norton
|
||||||
|
- [Growing developers with Drupal](https://drupalcamp.london/session/growing-developers-drupal)
|
||||||
|
by Fran Garcia-Linares (fjgarlin)
|
||||||
|
- [How to make it easier for newcomers to get involved in Drupal](https://drupalcamp.london/session/how-make-it-easier-newcomers-get-involved-drupal)
|
||||||
|
by heather
|
||||||
|
- [Let’s take the best route - Exploring Drupal 8 Routing System](https://drupalcamp.london/session/lets-take-best-route-exploring-drupal-8-routing-system)
|
||||||
|
by surbhi
|
||||||
|
- [New recipe of Decoupling: Drupal 8, Symfony and Slim Framework](https://drupalcamp.london/session/new-recipe-decoupling-drupal-8-symfony-and-slim-framework)
|
||||||
|
by Jyoti Singh
|
||||||
|
- [Plugin API by examples](https://drupalcamp.london/session/plugin-api-examples)
|
||||||
|
by Gabriele (gambry)
|
||||||
|
- [Value of mentorship in the community](https://drupalcamp.london/session/value-mentorship-community)
|
||||||
|
by Hussain Abbas (hussainweb)
|
||||||
|
- [Warden - Helping Drupal Agencies Sleep at Night](https://drupalcamp.london/session/warden-helping-drupal-agencies-sleep-night)
|
||||||
|
by Mike Davis
|
||||||
|
|
||||||
|
Unfortunately there are some time slots where I’d like to see more than one of
|
||||||
|
the talks (including when I’m going to be speaking). This regularly happens at
|
||||||
|
conferences, but I’ll look forward to watching those on [YouTube][7] after the
|
||||||
|
event.
|
||||||
|
|
||||||
|
I’m also looking forward to catching up with former colleagues, spending some
|
||||||
|
time in the "hallway track" and hopefully doing some sprinting too!
|
||||||
|
|
||||||
|
## Finally
|
||||||
|
|
||||||
|
For nostalgia, [here’s the blog post][0] that I wrote before I attended my first
|
||||||
|
DrupalCamp London.
|
||||||
|
|
||||||
|
See everyone this weekend!
|
||||||
|
|
||||||
|
[0]: {{site.url}}/blog/2014/02/09/drupalcamp-london-2014
|
||||||
|
[1]: https://drupalcamp.london
|
||||||
|
[2]: {{site.companies.microserve.url}}
|
||||||
|
[3]: {{site.url}}/talks/git-flow
|
||||||
|
[4]: {{site.url}}/talks/deploying-drupal-fabric
|
||||||
|
[5]: http://ryanszrama.com
|
||||||
|
[6]: https://commerceguys.com
|
||||||
|
[7]: https://www.youtube.com/channel/UCsaB96zszIP4Y3czs-ndiIA
|
67
source/_posts/mediacurrent-contrib-half-hour-is-back.md
Normal file
67
source/_posts/mediacurrent-contrib-half-hour-is-back.md
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
---
|
||||||
|
title: Yay, the Mediacurrent Contrib Half Hour is Back!
|
||||||
|
date: 2018-03-02
|
||||||
|
excerpt: Mediacurrent’s "contrib half hour sessions" are back.
|
||||||
|
tags:
|
||||||
|
- contribution
|
||||||
|
- drupal
|
||||||
|
- open-source
|
||||||
|
has_tweets: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Back in November, [Mediacurrent introduced][1] the contrib half hour - a weekly
|
||||||
|
online meeting to provide guidance and assistance on contributing to Drupal and
|
||||||
|
Drupal projects. A range of topics were covered in the first few sessions,
|
||||||
|
including finding and testing bug fixes, Composer, Drush, and how to re-roll
|
||||||
|
patches.
|
||||||
|
|
||||||
|
From Damien's [introductory blog post][2]:
|
||||||
|
|
||||||
|
> Not sure what this whole "patch" thing is? Have a core change that you can't
|
||||||
|
> quite finish? Running into a problem with a contrib module, or a theme, or a
|
||||||
|
> 3rd party library, and not sure how to fix it? New to maintaining a module and
|
||||||
|
> unsure of what to do next? Wondering how to get your module through the
|
||||||
|
> security opt-in process? Is your project's issue queue getting you down? Join
|
||||||
|
> us every Thursday at noon EST for the Mediacurrent Contrib Half Hour where
|
||||||
|
> we'll be available to help solve contrib challenges.
|
||||||
|
>
|
||||||
|
> Each week we'll host a live meeting to give step-by-step guidance on some best
|
||||||
|
> practices for contributing to Drupal, and provide Q and A assistance for our
|
||||||
|
> favorite open source (OSS) content management system (CMS). The meetings will
|
||||||
|
> be lead by yours truly, Damien McKenna, a prolific contributor to the Drupal
|
||||||
|
> community, and my coworkers here at Mediacurrent.
|
||||||
|
|
||||||
|
There is also an [updates blog post][3] that continues to show the latest
|
||||||
|
information, and the video recordings are [uploaded to YouTube][0] after the
|
||||||
|
session. Here is the first one from November:
|
||||||
|
|
||||||
|
<!-- <div class="talk-video mb-4">
|
||||||
|
<iframe width="678" height="408" src="//www.youtube.com/embed/8xHE5y1rA1g" frameborder="0" allowfullscreen></iframe>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
I enjoyed watching the first few videos, as I’m always interested in
|
||||||
|
contribution to Drupal and open-source and how to encourage it, but then no new
|
||||||
|
videos were uploaded for a while and I hoped that it hadn’t faded away.
|
||||||
|
|
||||||
|
I’m glad to see today that it’s back and that all of the previous videos have
|
||||||
|
been uploaded and added to the [YouTube playlist][0], and that [on the update
|
||||||
|
post][3] there are scheduled topics for the rest of this month including
|
||||||
|
documentation and automated testing.
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<blockquote class="twitter-tweet" data-cards="hidden" data-lang="en"><p lang="en" dir="ltr">All of the <a href="https://twitter.com/mediacurrent?ref_src=twsrc%5Etfw">@mediacurrent</a> <a href="https://twitter.com/hashtag/ContribHalfHour?src=hash&ref_src=twsrc%5Etfw">#ContribHalfHour</a> videos have been uploaded to our Youtube channel: <a href="https://t.co/1sWZT5sRSN">https://t.co/1sWZT5sRSN</a><br>Note: I accidentally forgot to save the Feb 22nd video, sorry :-\</p>— Damien McKenna (@DamienMcKenna) <a href="https://twitter.com/DamienMcKenna/status/969668677980315649?ref_src=twsrc%5Etfw">March 2, 2018</a></blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
I do enjoy watching these, and I like both the presentation and Q&A format that
|
||||||
|
they alternate between. I’ll look forward to catching up over the next few days,
|
||||||
|
and to hopefully seeing them continue to be uploaded after future meetings.
|
||||||
|
|
||||||
|
Thanks Damien and Mediacurrent!
|
||||||
|
|
||||||
|
[0]: https://www.youtube.com/playlist?list=PLu-MxhbnjI9rHroPvZO5LEUhr58Yl0j_F
|
||||||
|
[1]:
|
||||||
|
https://www.mediacurrent.com/blog/introducing-mediacurrent-contrib-half-hour
|
||||||
|
[2]:
|
||||||
|
https://www.mediacurrent.com/blog/introducing-mediacurrent-contrib-half-hour
|
||||||
|
[3]:
|
||||||
|
https://www.mediacurrent.com/blog/updates-mediacurrent-contrib-half-hour-weekly-meeting
|
97
source/_posts/minimum-core-version.md
Normal file
97
source/_posts/minimum-core-version.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
title: How to Define a Minimum Drupal Core Version
|
||||||
|
date: 2015-04-03
|
||||||
|
excerpt: How to define a minimum Drupal core version for your module or theme.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
meta:
|
||||||
|
og:
|
||||||
|
title: 'How to Define a Minimum Drupal Core Version'
|
||||||
|
excerpt:
|
||||||
|
'How to define a minimum Drupal core version for your module or theme.'
|
||||||
|
type: article
|
||||||
|
---
|
||||||
|
|
||||||
|
This week, my first code patch was
|
||||||
|
[committed to Drupal core](https://www.drupal.org/node/2394517#comment-9773143).
|
||||||
|
The patch adds the `user_has_role()` function to the user module, to simplify
|
||||||
|
the way to check whether a user in Drupal has been assigned a specific role.
|
||||||
|
This is something that I normally write a custom function for each project, but
|
||||||
|
it's now available in Drupal core as of
|
||||||
|
[7.36](https://www.drupal.org/drupal-7.36-release-notes).
|
||||||
|
|
||||||
|
But what if someone is using a core version less than 7.36 and tries using the
|
||||||
|
function? The site would return an error because that function wouldn't exist.
|
||||||
|
|
||||||
|
If you're building a new Drupal site, then I'd assume that you're using a latest
|
||||||
|
version of core, or you have the opportunity to update it when needed. But what
|
||||||
|
if you're writing a contrib module? How can you be sure that the correct minimum
|
||||||
|
version of core?
|
||||||
|
|
||||||
|
## Setting Dependencies
|
||||||
|
|
||||||
|
What I'm going to be doing for my contrib projects is defining a minimum version
|
||||||
|
of Drupal core that the module is compatible with. If this dependency isn't met,
|
||||||
|
the module won't be able to be enabled. This is done within your module's .info
|
||||||
|
file.
|
||||||
|
|
||||||
|
### Adding a Simple Dependency
|
||||||
|
|
||||||
|
You can define a simple dependency for your module by adding a line this this to
|
||||||
|
your project's .info file:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
dependencies[] = views
|
||||||
|
```
|
||||||
|
|
||||||
|
This would make your module dependant on having the
|
||||||
|
[Views](https://www.drupal.org/project/views) module present and enabled, which
|
||||||
|
you'd need if you were including views as part of your module, for example.
|
||||||
|
|
||||||
|
### Adding a Complex Dependency
|
||||||
|
|
||||||
|
In the previous example, our module would enable if _any_ version of Views was
|
||||||
|
enabled, but we need to specify a specific version. We can do this by including
|
||||||
|
version numbers within the dependencies field in the following format:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
dependencies[] = modulename (major.minor)
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be a for a specific module release or a branch name:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
dependencies[] = modulename (1.0)
|
||||||
|
dependencies[] = modulename (1.x)
|
||||||
|
```
|
||||||
|
|
||||||
|
We can also use the following as part of the field for extra granularity:
|
||||||
|
|
||||||
|
- = or == equals (this is the default)
|
||||||
|
- > greater than
|
||||||
|
- < lesser than
|
||||||
|
- > = greater than or equal to
|
||||||
|
- <= lesser than or equal to
|
||||||
|
- != not equal to
|
||||||
|
|
||||||
|
In the original scenario, we want to specify that the module can only be enabled
|
||||||
|
on Drupal core 7.36 or later. To do this, we can use the "greater than or equal
|
||||||
|
to" option.
|
||||||
|
|
||||||
|
```language-ini
|
||||||
|
dependencies[] = system (>=7.36)
|
||||||
|
```
|
||||||
|
|
||||||
|
Because we need to check for Drupal's core version, we're using the system
|
||||||
|
module as the dependency and specifying that it needs to be either equal to or
|
||||||
|
greater than 7.36. If this dependency is not met, e.g. Drupal 7.35 is being
|
||||||
|
used, then the module cannot be enabled rather than showing a function not found
|
||||||
|
error for `user_has_role()` when it is called.
|
||||||
|
|
||||||
|
![A screenshot of the modules page showing System as a dependency for a custom module.](/images/blog/minimum-drupal-version-d7.png)
|
||||||
|
|
||||||
|
## External Links
|
||||||
|
|
||||||
|
- [Writing module .info files (Drupal 7.x)](https://www.drupal.org/node/542202#dependencies)
|
10
source/_posts/my-first-blog-post-published-for-inviqa.md
Normal file
10
source/_posts/my-first-blog-post-published-for-inviqa.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: My first blog post published for Inviqa
|
||||||
|
excerpt: My first blog post has been published on the inviqa.com website.
|
||||||
|
date: 2020-04-29
|
||||||
|
tags: [drupal, testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
My first blog post was published on the Inviqa website last week. Is an introduction to automated testing in Drupal, which also includes a recap of the workshop that I recently gave at DrupalCamp London.
|
||||||
|
|
||||||
|
The blog post can be found at <https://inviqa.com/blog/drupal-automated-testing-introduction>, and there's more information about the workshop specifically at <https://github.com/opdavies/workshop-drupal-automated-testing>.
|
24
source/_posts/my-new-drupal-modules.md
Normal file
24
source/_posts/my-new-drupal-modules.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: My new Drupal modules
|
||||||
|
date: 2012-07-12
|
||||||
|
excerpt: After a busy few days, I've released two new contrib Drupal modules.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-modules
|
||||||
|
- drupal-6
|
||||||
|
- drupal-7
|
||||||
|
- drupal-planet
|
||||||
|
- accessibility
|
||||||
|
---
|
||||||
|
|
||||||
|
After a busy few days, I've released two new contrib Drupal modules.
|
||||||
|
|
||||||
|
- [Block Aria Landmark Roles](http://drupal.org/project/block_aria_landmark_roles) -
|
||||||
|
Inspired by [Block Class](http://drupal.org/project/block_class), this module
|
||||||
|
adds additional elements to the block configuration forms that allow users to
|
||||||
|
assign a ARIA landmark role to a block.
|
||||||
|
- [Nomensa Accessible Media Player](http://drupal.org/project/nomensa_amp) -
|
||||||
|
Provides integration with Nomensa's
|
||||||
|
[Accessible Media Player](https://github.com/nomensa/Accessible-Media-Player).
|
||||||
|
|
||||||
|
Documentation for both to follow shortly on Drupal.org.
|
111
source/_posts/my-sublime-text-2-settings.md
Normal file
111
source/_posts/my-sublime-text-2-settings.md
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
title: My Sublime Text 2 settings
|
||||||
|
date: 2012-10-25
|
||||||
|
excerpt:
|
||||||
|
<a href="http://www.sublimetext.com/2" title="Sublime Text 2">Sublime Text
|
||||||
|
2</a> has been my text editor of choice for the past few months, and I use it
|
||||||
|
at home, in work, and on any virtual machines that I run. So rather than
|
||||||
|
having to manually re-enter my settings each time, I thought that I'd document
|
||||||
|
them here for future reference.
|
||||||
|
tags:
|
||||||
|
- sublime-text
|
||||||
|
---
|
||||||
|
|
||||||
|
[Sublime Text 2](http://www.sublimetext.com/2) has been my text editor of choice
|
||||||
|
for the past few months, and I use it at home, in work, and on any virtual
|
||||||
|
machines that I run. So rather than having to manually re-enter my settings each
|
||||||
|
time, I thought that I'd document them here for future reference.
|
||||||
|
|
||||||
|
These preferences ensure that the code is compliant with
|
||||||
|
[Drupal coding standards](http://drupal.org/coding-standards 'Drupal coding standards on Drupal.org') -
|
||||||
|
using two spaces instead of a tab, no trailing whitespace, blank line at the end
|
||||||
|
of a file etc.
|
||||||
|
|
||||||
|
## Preferences
|
||||||
|
|
||||||
|
These can be changed by going to Preferences > Settings - User.
|
||||||
|
|
||||||
|
```language-json
|
||||||
|
{
|
||||||
|
"color_scheme": "Packages/Theme - Aqua/Color Schemes/Tomorrow Night Aqua.tmTheme",
|
||||||
|
"default_line_ending": "unix",
|
||||||
|
"ensure_newline_at_eof_on_save": true,
|
||||||
|
"fallback_encoding": "UTF-8",
|
||||||
|
"file_exclude_patterns":
|
||||||
|
[
|
||||||
|
"*.pyc",
|
||||||
|
"*.pyo",
|
||||||
|
"*.exe",
|
||||||
|
"*.dll",
|
||||||
|
"*.obj",
|
||||||
|
"*.o",
|
||||||
|
"*.a",
|
||||||
|
"*.lib",
|
||||||
|
"*.so",
|
||||||
|
"*.dylib",
|
||||||
|
"*.ncb",
|
||||||
|
"*.sdf",
|
||||||
|
"*.suo",
|
||||||
|
"*.pdb",
|
||||||
|
"*.idb",
|
||||||
|
".DS_Store",
|
||||||
|
"*.class",
|
||||||
|
"*.psd",
|
||||||
|
"*.db",
|
||||||
|
"*.sublime*"
|
||||||
|
],
|
||||||
|
"folder_exclude_patterns":
|
||||||
|
[
|
||||||
|
".svn",
|
||||||
|
".git",
|
||||||
|
".hg",
|
||||||
|
"CVS",
|
||||||
|
"FirePHPCore"
|
||||||
|
],
|
||||||
|
"font_options":
|
||||||
|
[
|
||||||
|
"no_bold",
|
||||||
|
"no_italic"
|
||||||
|
],
|
||||||
|
"font_size": 16.0,
|
||||||
|
"highlight_line": true,
|
||||||
|
"ignored_packages":
|
||||||
|
[
|
||||||
|
],
|
||||||
|
"line_padding_bottom": 1,
|
||||||
|
"rulers":
|
||||||
|
[
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"save_on_focus_lost": true,
|
||||||
|
"shift_tab_unindent": true,
|
||||||
|
"tab_size": 2,
|
||||||
|
"theme": "Soda Light.sublime-theme",
|
||||||
|
"translate_tabs_to_spaces": true,
|
||||||
|
"trim_automatic_white_space": true,
|
||||||
|
"trim_trailing_white_space_on_save": true,
|
||||||
|
"word_wrap": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key bindings
|
||||||
|
|
||||||
|
These can be changed by going to Preferences > Key Bindings - User.
|
||||||
|
|
||||||
|
```language-json
|
||||||
|
[
|
||||||
|
{ "keys": ["alt+s"], "command": "toggle_side_bar" },
|
||||||
|
{ "keys": ["alt+r"], "command": "reindent" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
These are the packages that I currently have installed.
|
||||||
|
|
||||||
|
- [DocBlockr](https://github.com/spadgos/sublime-jsdocs 'DocBlockr on GitHub')
|
||||||
|
- [Drupal API](https://github.com/BrianGilbert/Sublime-Text-2-Goto-Drupal-API)
|
||||||
|
- [LESS](https://github.com/danro/LESS-sublime)
|
||||||
|
- [Package Control](http://wbond.net/sublime_packages/package_control)
|
||||||
|
- [Sublime CodeIntel](http://github.com/Kronuz/SublimeCodeIntel)
|
||||||
|
- [Theme - Soda](https://github.com/buymeasoda/soda-theme)
|
58
source/_posts/nginx-redirects-with-query-string-arguments.md
Normal file
58
source/_posts/nginx-redirects-with-query-string-arguments.md
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
title: Nginx Redirects With Query String Arguments
|
||||||
|
date: 2017-01-31
|
||||||
|
excerpt:
|
||||||
|
How to redirect from an old domain to a new one, and also to redirect from the
|
||||||
|
root example.com domain to the canonical www subdomain.
|
||||||
|
tags: [nginx]
|
||||||
|
---
|
||||||
|
|
||||||
|
This is an example of how my Nginx configuration looked to redirect from an old
|
||||||
|
domain to a new one, and also to redirect from the root `example.com` domain to
|
||||||
|
the canonical `www` subdomain.
|
||||||
|
|
||||||
|
```language-nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
server_name example.com;
|
||||||
|
server_name my-old-domain.com;
|
||||||
|
server_name www.my-old-domain.com;
|
||||||
|
|
||||||
|
return 301 https://www.example.com$uri;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It also redirects the URI value, e.g. from `http://example.com/test` to
|
||||||
|
`http://example.com/test`, but I noticed recently though that any the query
|
||||||
|
string would be lost - e.g. `http://example.com/?test` would redirect to
|
||||||
|
`http://www.example.com` and the `?test` would be dropped. The application that
|
||||||
|
I built references images based on the query string, so I wanted these to be
|
||||||
|
included within the redirect.
|
||||||
|
|
||||||
|
This was fixed by making a small change to my `return` statement.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```language-nginx
|
||||||
|
return 301 https://www.example.com$uri;
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```language-nginx
|
||||||
|
return 301 https://www.example.com$uri$is_args$args;
|
||||||
|
```
|
||||||
|
|
||||||
|
`$is_args` is an empty string if there are no arguments, or a `?` to signify the
|
||||||
|
start of the query string. `$args` then adds the arguments (`$query_string`
|
||||||
|
could also be used with the same result).
|
||||||
|
|
||||||
|
Here is an demo of it working on this website:
|
||||||
|
|
||||||
|
![](/images/blog/nginx-redirect-with-args.gif)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Query string](https://en.wikipedia.org/wiki/Query_string)
|
||||||
|
- [Nginx ngx_http_core_module](http://nginx.org/en/docs/http/ngx_http_core_module.html)
|
155
source/_posts/null-users-and-system-users-in-drupal.md
Normal file
155
source/_posts/null-users-and-system-users-in-drupal.md
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
---
|
||||||
|
title: Null Users and System Users in Drupal
|
||||||
|
date: 2018-08-16
|
||||||
|
excerpt: Announcing the Null User and System User modules.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-7
|
||||||
|
- drupal-8
|
||||||
|
- drupal-modules
|
||||||
|
- drupal-planet
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
Have you ever needed to have a 'special user' to perform tasks on your Drupal
|
||||||
|
site, such as performing actions based on an API request, or for sending an
|
||||||
|
internal site message?
|
||||||
|
|
||||||
|
If you just create a new user, how do you identify that user going forward? Do
|
||||||
|
you hard-code the 'magic' user ID in your custom code? What if the user has a
|
||||||
|
different ID on different environments of your site? You could declare it in
|
||||||
|
each environment’s settings file and retrieve it from there, but what then if
|
||||||
|
you need to do the same on another site? That would mean some duplication of
|
||||||
|
code - and something that could have been abstracted and re-used.
|
||||||
|
|
||||||
|
I had to do this recently, and rather than just duplicate the code I decided to
|
||||||
|
make it into it’s own module - which then became two modules.
|
||||||
|
|
||||||
|
## System users
|
||||||
|
|
||||||
|
The [System User module][1] provides a re-usable, generic way to denote users as
|
||||||
|
'system users', which is not specific to a certain site or environment as this
|
||||||
|
is value is stored against each individual user in the database.
|
||||||
|
|
||||||
|
'System user' is a term used in Linux, which I thought also applies well to this
|
||||||
|
scenario.
|
||||||
|
|
||||||
|
From <https://www.ssh.com/iam/user/system-account>:
|
||||||
|
|
||||||
|
> A system account is a user account that is created by an operating system
|
||||||
|
> during installation and that is used for operating system defined purposes.
|
||||||
|
> System accounts often have predefiend user ids. Examples of system accounts
|
||||||
|
> include the root account in Linux.
|
||||||
|
|
||||||
|
A system user isn’t an account that we’d expect a person to log in with and
|
||||||
|
perform routine tasks like updating content, but rather for the system (site) to
|
||||||
|
use to perform tasks like the earlier examples.
|
||||||
|
|
||||||
|
### Declaring a user as a system user
|
||||||
|
|
||||||
|
System User module adds a base field to Drupal’s User entity, which determines
|
||||||
|
whether or not each user is a system user - i.e. if this field is `TRUE`, that
|
||||||
|
user is a system user. This means that users can easily be queried to identify
|
||||||
|
which are system users, without having to rely on magic, environment and site
|
||||||
|
specific user IDs. This also means that we can have multiple system users, if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
![](/images/blog/null-users-system-users/drupal-8-users-field-data-table.png){.border
|
||||||
|
.p-1}
|
||||||
|
|
||||||
|
In the Drupal 8 version of the module, a `SystemUser` is a custom entity, that
|
||||||
|
contains it’s own `create` method for creating new system users. This is a
|
||||||
|
essentially a wrapper around `User::create()` that automatically sets the value
|
||||||
|
of the system user field as part of the creation.
|
||||||
|
|
||||||
|
The original intention is that system users would always be created manually in
|
||||||
|
an custom install or update hook, however since releasing the module, I’ve also
|
||||||
|
added an install hook to the module to automatically create a new system user
|
||||||
|
when the module is installed, basing the username on the site name.
|
||||||
|
|
||||||
|
There is also an open issue to add a Drush command to create a new system user,
|
||||||
|
and I’d imagine I’ll also add a Drupal Console command too.
|
||||||
|
|
||||||
|
### Retrieving system users
|
||||||
|
|
||||||
|
Whilst you could easily write your own query that retrieves users based on the
|
||||||
|
value of the system user field, but the module contains a `SystemUserManager`
|
||||||
|
service that contains methods to do so. It also provides a static helper class
|
||||||
|
that determines if a specified user is a system user by checking the value of
|
||||||
|
the system user field.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Retrieve the first system user.
|
||||||
|
$system_user = $this->systemUserManager->getFirst();
|
||||||
|
|
||||||
|
// Is the specified user a system user?
|
||||||
|
$is_system_user = SystemUserManager::isSystemUser($user);
|
||||||
|
```
|
||||||
|
|
||||||
|
But what do we return if there are no system users? You could return `NULL` or
|
||||||
|
`FALSE`, but I decided to take a different approach, which became the second
|
||||||
|
module.
|
||||||
|
|
||||||
|
## Null users
|
||||||
|
|
||||||
|
The [Null User module][2] is an implementation of the [null object pattern][3]
|
||||||
|
for users in Drupal 8. In this case, a [NullUser][4] is an extension of Drupal’s
|
||||||
|
`AnonymousUserSession`, which means that it inherits sensible defaults to return
|
||||||
|
for a non-existent User. Though, through inheritance, the `id`, `getRoles` and
|
||||||
|
`hasPermission` methods are overridden to return relevant values.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
use Drupal\Core\Session\AnonymousUserSession;
|
||||||
|
|
||||||
|
class NullUser extends AnonymousUserSession {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Null User module is a dependency of System User in Drupal 8, so When no system
|
||||||
|
user is found from the `getFirst()` method, a `NullUser` is returned. Whilst I
|
||||||
|
could alternatively have returned `NULL` or `FALSE`, we then would need to check
|
||||||
|
if the returned value was an object or not before calling methods on it.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$system_user = $this->systemUserManager->getFirst(); // Returns NULL or FALSE.
|
||||||
|
|
||||||
|
// Need to check if a user was returned or not.
|
||||||
|
if (!$system_user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($system_user->isActive()) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because instead we’re returning a `NullUser`, which through class inheritance
|
||||||
|
has the same methods and properties as a regular user, there is no need to do
|
||||||
|
the additional check as you will always receive a relevant object, and the
|
||||||
|
expected methods will always be present.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
$system_user = $this->systemUserManager->getFirst(); // Returns a NullUser.
|
||||||
|
|
||||||
|
if ($system_user->isActive()) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means we have less code, which also is simpler and more readable.
|
||||||
|
|
||||||
|
System User module is the only one that I’m aware of that makes use of Null
|
||||||
|
User, but I’ve added a list to the [project page][2] so let me know if you can
|
||||||
|
think of any others.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Null object pattern][3]
|
||||||
|
- [Null User module][2]
|
||||||
|
- [System User module][1]
|
||||||
|
|
||||||
|
[1]: https://www.drupal.org/project/system_user
|
||||||
|
[2]: https://www.drupal.org/project/null_user
|
||||||
|
[3]: https://en.wikipedia.org/wiki/Null_object_pattern
|
||||||
|
[4]: http://cgit.drupalcode.org/null_user/tree/src/NullUser.php?h=8.x-1.x
|
23
source/_posts/open-sublime-text-2-mac-os-x-command-line.md
Normal file
23
source/_posts/open-sublime-text-2-mac-os-x-command-line.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: Open Sublime Text 2 from the Mac OS X Command Line
|
||||||
|
date: 2012-11-17
|
||||||
|
excerpt: How to open Sublime Text from the command line.
|
||||||
|
tags:
|
||||||
|
- sublime-text
|
||||||
|
- mac-os-x
|
||||||
|
- terminal
|
||||||
|
---
|
||||||
|
|
||||||
|
How to open Sublime Text from the command line.
|
||||||
|
|
||||||
|
Paste the following code into the Mac OS X Terminal, assuming that you've
|
||||||
|
installed Sublime Text 2 into the /Applications folder.
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
$ ln -s "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl" ~/bin/sublime
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can type `sublime <filename>` open a file or directory in Sublime Text,
|
||||||
|
or `sublime .` to open the current directory.
|
||||||
|
|
||||||
|
You can also type `sublime --help` to see a list of the available commands.
|
100
source/_posts/pantheon-settings-files.md
Normal file
100
source/_posts/pantheon-settings-files.md
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
title: Include environment-specific settings files on Pantheon
|
||||||
|
date: 2014-11-27
|
||||||
|
excerpt: How to load a different settings file per environment on Pantheon.
|
||||||
|
tags:
|
||||||
|
- drupal
|
||||||
|
- drupal-planet
|
||||||
|
- pantheon
|
||||||
|
- settings.php
|
||||||
|
---
|
||||||
|
|
||||||
|
I was recently doing some work on a site hosted on
|
||||||
|
[Pantheon](http://getpantheon.com) and came across an issue, for which part of
|
||||||
|
the suggested fix was to ensure that the `$base_url` variable was explicitly
|
||||||
|
defined within settings.php (this is also best practice on all Drupal sites).
|
||||||
|
|
||||||
|
The way that was recommended was by using a `switch()` function based on
|
||||||
|
Pantheon's environment variable. For example:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
switch ($_SERVER['PANTHEON_ENVIRONMENT']) {
|
||||||
|
case 'dev':
|
||||||
|
// Development environment.
|
||||||
|
$base_url = 'dev-my-site.gotpantheon.com';
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case 'test':
|
||||||
|
// Testing environment.
|
||||||
|
$base_url = 'test-my-site.gotpantheon.com';
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case 'live':
|
||||||
|
// Production environment.
|
||||||
|
$base_url = 'live-my-site.gotpantheon.com';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Whilst this works, it doesn't conform to the DRY (don't repeat yourself)
|
||||||
|
principle and means that you also might get a rather long and complicated
|
||||||
|
settings file, especially when you start using multiple switches and checking
|
||||||
|
for the value of the environment multiple times.
|
||||||
|
|
||||||
|
My alternative solution to this is to include an environment-specific settings
|
||||||
|
file.
|
||||||
|
|
||||||
|
To do this, add the following code to the bottom of settings.php:
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
if (isset($_SERVER['PANTHEON_ENVIRONMENT'])) {
|
||||||
|
if ($_SERVER['PANTHEON_ENVIRONMENT'] != 'live') {
|
||||||
|
// You can still add things here, for example to apply to all sites apart
|
||||||
|
// from production. Mail reroutes, caching settings etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include an environment-specific settings file, for example
|
||||||
|
// settings.dev.php, if one exists.
|
||||||
|
$environment_settings = __DIR__ . '/settings.' . $_SERVER['PANTHEON_ENVIRONMENT'] . '.php';
|
||||||
|
if (file_exists($environment_settings)) {
|
||||||
|
include $environment_settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that rather than having one long file, each environment has it's own
|
||||||
|
dedicated settings file that contains it's own additional configuration. This is
|
||||||
|
much easier to read and make changes to, and also means that less code is loaded
|
||||||
|
and parsed by PHP. Settings that apply to all environments are still added to
|
||||||
|
settings.php.
|
||||||
|
|
||||||
|
Below this, I also include a
|
||||||
|
[similar piece of code](/blog/include-local-drupal-settings-file-environment-configuration-and-overrides/)
|
||||||
|
to include a settings.local.php file. The settings.php file then gets committed
|
||||||
|
into the [Git](http://git-scm.com) repository.
|
||||||
|
|
||||||
|
Within the sites/default directory, I also include an example file
|
||||||
|
(example.settings.env.php) for reference. This is duplicated, renamed and
|
||||||
|
populated accordingly.
|
||||||
|
|
||||||
|
```language-php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a specific settings file, just for the x environment. Any settings
|
||||||
|
* defined here will be included after those in settings.php.
|
||||||
|
*
|
||||||
|
* If you have also added a settings.local.php file, that will override any
|
||||||
|
* settings stored here.
|
||||||
|
*
|
||||||
|
* No database credentials should be stored in this file as these are included
|
||||||
|
* automatically by Pantheon.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$base_url = '';
|
||||||
|
```
|
||||||
|
|
||||||
|
The environment specific files are also committed into Git and pushed to
|
||||||
|
Pantheon, and are then included automatically on each environment.
|
32
source/_posts/php-apps-subdirectory-nginx.md
Normal file
32
source/_posts/php-apps-subdirectory-nginx.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
title:
|
||||||
|
How to put your PHP application in a subdirectory of another site with Nginx
|
||||||
|
date: 2018-03-12
|
||||||
|
excerpt:
|
||||||
|
How to configure Nginx to serve a PHP application from within a subdirectory
|
||||||
|
of another.
|
||||||
|
tags:
|
||||||
|
- nginx
|
||||||
|
- php
|
||||||
|
---
|
||||||
|
|
||||||
|
In January, [Chris Fidao][0] posted a video to [Servers for Hackers][1] showing
|
||||||
|
how to put different PHP applications in different subdirectories and have them
|
||||||
|
serving on different paths with Nginx. I’ve had to do this a few times
|
||||||
|
previously, and it’s great to have this video as a reference.
|
||||||
|
|
||||||
|
> In this video, we work through how to put your PHP application in a
|
||||||
|
> subdirectory of another site.
|
||||||
|
>
|
||||||
|
> For example, we may have an application running at example.org but need a
|
||||||
|
> second application running at example.org/blog.
|
||||||
|
>
|
||||||
|
> This feels like it should be simple, but it turns out to be more complex and
|
||||||
|
> fraught with confusing Nginx configurations! To make matter worse (or,
|
||||||
|
> perhaps, to illustrate this point), a quick Google search reveals a TON of
|
||||||
|
> confusing, non-working examples.
|
||||||
|
|
||||||
|
<https://serversforhackers.com/c/nginx-php-in-subdirectory>
|
||||||
|
|
||||||
|
[0]: https://twitter.com/fideloper
|
||||||
|
[1]: https://serversforhackers.com
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
title: Presenting on Tailwind CSS and Ansible at CMS Philly
|
||||||
|
excerpt: I'll be presenting on Tailwind CSS and deployments with Ansible at CMS Philly on May 1st.
|
||||||
|
date: 2020-04-24
|
||||||
|
tags: [conference, speaking, tailwind-css, ansible, ansistrano, drupal, drupal-planet]
|
||||||
|
---
|
||||||
|
|
||||||
|
![](/images/blog/presenting-on-tailwind-css-and-ansible-at-cms-philly/cms-philly-logo.png){.mx-auto}
|
||||||
|
|
||||||
|
I'm happy to be presenting two talks remotely at this year's [CMS Philly](https://cmsphilly.org) conference (formerly [Drupaldelphia](https://www.drupaldelphia.org)).
|
||||||
|
|
||||||
|
The first talk is [Deploying PHP applications with Ansible, Ansible Vault and Ansistrano](/talks/deploying-php-ansible-ansistrano) at 1pm (6pm UK time) where I'll be doing an introduction to Ansible and show how to use Ansistrano to do deploy a Drupal 8 application.
|
||||||
|
|
||||||
|
The second talk is [Taking Flight with Tailwind CSS](/talks/taking-flight-with-tailwind-css) at 2pm (7pm UK time) where I'll show how to configure and use Tailwind CSS.
|
||||||
|
|
||||||
|
CMS Philly is happening virtually on Friday, May 1st via GoToWebinar.
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
title: Prevent Apache from displaying text files within a web browser
|
||||||
|
date: 2012-05-23
|
||||||
|
excerpt:
|
||||||
|
How to prevent Apache from displaying the contents of files like
|
||||||
|
CHANGELOG.txt.
|
||||||
|
tags:
|
||||||
|
- apache
|
||||||
|
- code
|
||||||
|
- drupal
|
||||||
|
---
|
||||||
|
|
||||||
|
When you download [Drupal](http://drupal.org/project/drupal), there are several
|
||||||
|
text files that are placed in the root of your installation. You don't want or
|
||||||
|
need these to be visible to anyone attempting to view them in a browser -
|
||||||
|
especially CHANGELOG.txt as that includes the exact version of Drupal you are
|
||||||
|
running and could therefore have security implications.
|
||||||
|
|
||||||
|
Rather than delete these files or change the file permissions manually for each
|
||||||
|
file, I can add the following lines into my VirtualHost configuration.
|
||||||
|
|
||||||
|
```language-apacheconf
|
||||||
|
<Files ~ "\.txt$">
|
||||||
|
Order deny,allow
|
||||||
|
Deny from all
|
||||||
|
</Files>
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents any files with a .txt extension from being accessed and rendered
|
||||||
|
in a web browser.
|
20
source/_posts/proctor-stevenson.md
Normal file
20
source/_posts/proctor-stevenson.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
title: Proctor & Stevenson
|
||||||
|
date: 2011-03-31
|
||||||
|
excerpt: I’m moving jobs.
|
||||||
|
tags:
|
||||||
|
- personal
|
||||||
|
---
|
||||||
|
|
||||||
|
2 weeks ago, I handed in my notice of resignation to
|
||||||
|
[Horse & Country TV](http://horseandcountry.tv)because I've been offered a new
|
||||||
|
role at [Proctor & Stevenson](http://proctors.co.uk) - a Marketing Design and
|
||||||
|
Communications agency in Bristol.
|
||||||
|
|
||||||
|
Proctors have an [extensive client list](http://www.proctors.co.uk/clients) -
|
||||||
|
including [BMW](http://www.proctors.co.uk/clients/bmw-financial-services),
|
||||||
|
[Panasonic](http://www.proctors.co.uk/clients/panasonic), the
|
||||||
|
[Open University](http://www.proctors.co.uk/clients/open-university) and
|
||||||
|
[VOSA](http://www.proctors.co.uk/clients/vosa), and it's going to be a fantastic
|
||||||
|
opportunity for me to continue expanding my skillset whilst gaining vital
|
||||||
|
experience.
|
17
source/_posts/proctors-hosting-next-drupal-meetup.md
Normal file
17
source/_posts/proctors-hosting-next-drupal-meetup.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: Proctors Hosting the next Drupal Meetup
|
||||||
|
date: 2011-05-20
|
||||||
|
excerpt:
|
||||||
|
Proctor & Stevenson are going to be hosting the next Bristol & South West
|
||||||
|
Drupal meetup.
|
||||||
|
tags:
|
||||||
|
- meetups
|
||||||
|
- drupal-bristol
|
||||||
|
---
|
||||||
|
|
||||||
|
My employer, [Proctor & Stevenson](http://www.proctors.co.uk), are going to be
|
||||||
|
hosting the next Bristol & South West Drupal meetup on the 25th May at our
|
||||||
|
offices.
|
||||||
|
|
||||||
|
You can [view more details](http://groups.drupal.org/node/147324), or register
|
||||||
|
[on our website](http://www.proctors.co.uk/Drupal-SWUG-Meetup).
|
137
source/_posts/psr4-autoloading-test-cases-drupal-7.md
Normal file
137
source/_posts/psr4-autoloading-test-cases-drupal-7.md
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
---
|
||||||
|
title: Using PSR-4 Autoloading for your Drupal 7 Test Cases
|
||||||
|
excerpt:
|
||||||
|
How to use the PSR-4 autoloading standard for Drupal 7 Simpletest test cases.
|
||||||
|
tags: [drupal, drupal-planet, drupal-7, testing, simpletest, php, psr]
|
||||||
|
date: 2020-02-04
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>{{ page.excerpt }}</p>
|
||||||
|
|
||||||
|
## The Traditional Way
|
||||||
|
|
||||||
|
The typical way of including test cases in Drupal 7 is to add one or more
|
||||||
|
classes within a `.test` file - e.g. `opdavies.test`. This would typically
|
||||||
|
include all of the different test cases for that module, and would be placed in
|
||||||
|
the root of the module’s directory alongside the `.info` and `.module` files.
|
||||||
|
|
||||||
|
In order to load the files, each file would need to be declared within the
|
||||||
|
`.info` file for the module.
|
||||||
|
|
||||||
|
There is a convention that if you have multiple tests for your project, these
|
||||||
|
can be split into different files and grouped within a `tests` directory.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
; Load a test file at the root of the module
|
||||||
|
files[] = opdavies.test
|
||||||
|
|
||||||
|
; Load a test file from within a subdirectory
|
||||||
|
files[] = tests/foo.test
|
||||||
|
files[] = tests/bar.test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the xautoload Module
|
||||||
|
|
||||||
|
Whilst splitting tests into separate files makes things more organised, each
|
||||||
|
file needs to be loaded separately. This can be made simpler by using the
|
||||||
|
[Xautoload module][], which supports wildcards when declaring files.
|
||||||
|
|
||||||
|
[xautoload module]: https://www.drupal.org/project/xautoload
|
||||||
|
|
||||||
|
```ini
|
||||||
|
files[] = tests/**/*.test
|
||||||
|
```
|
||||||
|
|
||||||
|
This would load all of the `.test` files within the tests directory.
|
||||||
|
|
||||||
|
## Using PSR-4 Autoloading
|
||||||
|
|
||||||
|
Another option is to use PSR-4 (or PSR-0) autoloading.
|
||||||
|
|
||||||
|
This should be a lot more familiar to those who have worked with Drupal 8,
|
||||||
|
Symfony etc, and means that each test case is in its own file which is cleaner,
|
||||||
|
files have the `.php` extension which is more standard, and the name of the file
|
||||||
|
matches the name of the test class for consistency.
|
||||||
|
|
||||||
|
To do this, create a `src/Tests` (PSR-4) or `lib/Drupal/{module_name}/Tests`
|
||||||
|
(PSR-0) directory within your module, and then add or move your test cases
|
||||||
|
there. Add the appropriate namespace for your module, and ensure that
|
||||||
|
`DrupalWebTestCase` or `DrupalUnitTestCase` is also namespaced.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Tests/Functional/OliverDaviesTest.php
|
||||||
|
|
||||||
|
namespace Drupal\opdavies\Tests\Functional;
|
||||||
|
|
||||||
|
class OliverDaviesTest extends \DrupalWebTestCase {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This also supports subdirectories, so you can group classes within `Functional`
|
||||||
|
and `Unit` directories if you like.
|
||||||
|
|
||||||
|
If you want to see an real-world example, see the Drupal 7 branch of the
|
||||||
|
[Override Node Options module][override_node_options].
|
||||||
|
|
||||||
|
[override_node_options]:
|
||||||
|
https://git.drupalcode.org/project/override_node_options/tree/7.x-1.x
|
||||||
|
|
||||||
|
### Digging into the simpletest_test_get_all function
|
||||||
|
|
||||||
|
This is the code within `simpletest.module` that makes this work:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// simpletest_test_get_all()
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
$module_dir = DRUPAL_ROOT . '/' . dirname($filename);
|
||||||
|
|
||||||
|
// Search both the 'lib/Drupal/mymodule' directory (for PSR-0 classes)
|
||||||
|
// and the 'src' directory (for PSR-4 classes).
|
||||||
|
foreach (array(
|
||||||
|
'lib/Drupal/' . $name,
|
||||||
|
'src',
|
||||||
|
) as $subdir) {
|
||||||
|
|
||||||
|
// Build directory in which the test files would reside.
|
||||||
|
$tests_dir = $module_dir . '/' . $subdir . '/Tests';
|
||||||
|
|
||||||
|
// Scan it for test files if it exists.
|
||||||
|
if (is_dir($tests_dir)) {
|
||||||
|
$files = file_scan_directory($tests_dir, '/.*\\.php/');
|
||||||
|
if (!empty($files)) {
|
||||||
|
foreach ($files as $file) {
|
||||||
|
|
||||||
|
// Convert the file name into the namespaced class name.
|
||||||
|
$replacements = array(
|
||||||
|
'/' => '\\',
|
||||||
|
$module_dir . '/' => '',
|
||||||
|
'lib/' => '',
|
||||||
|
'src/' => 'Drupal\\' . $name . '\\',
|
||||||
|
'.php' => '',
|
||||||
|
);
|
||||||
|
$classes[] = strtr($file->uri, $replacements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It looks for a the tests directory (`src/Tests` or
|
||||||
|
`lib/Drupal/{module_name}/Tests`) within the module, and then finds any `.php`
|
||||||
|
files within it. It then converts the file name into the fully qualified
|
||||||
|
(namespaced) class name and loads it automatically.
|
||||||
|
|
||||||
|
### Running the Tests
|
||||||
|
|
||||||
|
You can still run the tests from within the Simpletest UI, or from the command
|
||||||
|
line using `run-tests.sh`.
|
||||||
|
|
||||||
|
If you want to run a specific test case using the `--class` option, you will now
|
||||||
|
need to include the fully qualified name.
|
||||||
|
|
||||||
|
```
|
||||||
|
php scripts/run-tests.sh --class Drupal\\opdavies\\Tests\\Functional\\OliverDaviesTest
|
||||||
|
```
|
119
source/_posts/published-my-first-npm-package.md
Normal file
119
source/_posts/published-my-first-npm-package.md
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
---
|
||||||
|
title: Published my first NPM package
|
||||||
|
date: 2018-12-16
|
||||||
|
excerpt:
|
||||||
|
Yesterday I published my first module onto NPM, and it’s a plugin for Tailwind
|
||||||
|
CSS to be used alongside Vue.js.
|
||||||
|
tags:
|
||||||
|
- npm
|
||||||
|
- tailwind-css
|
||||||
|
- vuejs
|
||||||
|
---
|
||||||
|
|
||||||
|
Yesterday I published my first module onto NPM, and it’s a plugin for [Tailwind
|
||||||
|
CSS][tailwind] to be used alongside [Vue.js](https://vuejs.org).
|
||||||
|
|
||||||
|
The plugin adds classes for showing and hiding elements in different display
|
||||||
|
variations in combination with Vue's
|
||||||
|
[v-cloak directive](https://vuejs.org/v2/api/#v-cloak), which I originally saw
|
||||||
|
in [the first 'Building Kitetail' video](https://youtu.be/XUXpcbYQ_iQ?t=2360).
|
||||||
|
These are useful for when you want an element to be visible whilst Vue is
|
||||||
|
compiling, and hidden afterwards.
|
||||||
|
|
||||||
|
Here is the compiled CSS that is added by the plugin:
|
||||||
|
|
||||||
|
```css
|
||||||
|
[v-cloak] .v-cloak-block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-inline-flex {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] .v-cloak-invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-cloak-block,
|
||||||
|
.v-cloak-flex,
|
||||||
|
.v-cloak-inline,
|
||||||
|
.v-cloak-inline-block,
|
||||||
|
.v-cloak-inline-flex {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `v-cloak` directive exists on an element until Vue finishes compiling, after
|
||||||
|
which it is removed. Therefore adding a `v-cloak-block` class to an element will
|
||||||
|
make it `display: block` whilst Vue is compiling and the element is cloaked, and
|
||||||
|
`display: none` afterwards when the Vue markup is compiled and rendered.
|
||||||
|
|
||||||
|
In my `base.html.twig` template, I’ve added `v-cloak` to the wrapper div within
|
||||||
|
the `body`.
|
||||||
|
|
||||||
|
{% verbatim %}<div v-pre markdown="1">
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<body class="font-sans leading-normal">
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
{# ... #}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>{% endverbatim %}
|
||||||
|
|
||||||
|
Within my `navbar.html.twig` partial, I have a placeholder div that also
|
||||||
|
contains the site name, which is instantly visible but has the `v-cloak-block`
|
||||||
|
class so it’s hidden once Vue has compiled and the `Navbar` Vue component is
|
||||||
|
visible instead.
|
||||||
|
|
||||||
|
{% verbatim %}<div v-pre markdown="1">
|
||||||
|
|
||||||
|
```twig
|
||||||
|
<div class="border-bottom border-b border-gray-300 mb-6">
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div class="block py-5 v-cloak-block">
|
||||||
|
{{ site.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<navbar
|
||||||
|
site-name="{{ site.title }}"
|
||||||
|
page-url="{{ page.url }}"
|
||||||
|
></navbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>{% endverbatim %}
|
||||||
|
|
||||||
|
I was originally surprised that these classes weren’t included as part of
|
||||||
|
Tailwind or as part of an existing plugin, but as I’ve already used these styles
|
||||||
|
on several projects that include Vue.js with Symfony or Sculpin, it made sense
|
||||||
|
to extract it into a plugin and make it available as a npm package which I can
|
||||||
|
easily add to any project - as well as making it easier to maintain if I need to
|
||||||
|
add additional variations at a later point.
|
||||||
|
|
||||||
|
**You can view [the package on npmjs.com][npm], and [the code repository on
|
||||||
|
GitHub][github].**
|
||||||
|
|
||||||
|
[github]: https://github.com/opdavies/tailwindcss-vuejs
|
||||||
|
[npm]: https://www.npmjs.com/package/tailwindcss-vuejs
|
||||||
|
[tailwind]: https://tailwindcss.com
|
110
source/_posts/publishing-sculpin-sites-with-github-pages.md
Normal file
110
source/_posts/publishing-sculpin-sites-with-github-pages.md
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
---
|
||||||
|
title: Publishing Sculpin Sites with GitHub Pages
|
||||||
|
date: 2017-07-13
|
||||||
|
excerpt: How I moved my website to GitHub pages.
|
||||||
|
tags: [sculpin, php, github]
|
||||||
|
meta:
|
||||||
|
image:
|
||||||
|
url: '/images/blog/jackson-octocat.png'
|
||||||
|
type: 'image/png'
|
||||||
|
height: 200
|
||||||
|
width: 451
|
||||||
|
---
|
||||||
|
|
||||||
|
<p class="text-center" markdown="1">![](/images/blog/jackson-octocat.png)</p>
|
||||||
|
|
||||||
|
Earlier this week I moved this site from my personal Linode server to [GitHub
|
||||||
|
Pages][0].
|
||||||
|
|
||||||
|
This made sense as I already kept the source code in [on GitHub][1], the issue
|
||||||
|
was that GitHub Pages doesn’t know how to dynamically parse and generate a
|
||||||
|
Sculpin site like it does with some other static site generators. It can though
|
||||||
|
parse and serve HTML files, which is what Sculpin generates. It’s just a case of
|
||||||
|
how those files are added to GitHub.
|
||||||
|
|
||||||
|
I’ve seen different implementations of this, mostly where the Sculpin code is on
|
||||||
|
one branch, and the generated HTML code is on a separate `gh-pages` or `master`
|
||||||
|
branch (depending on your repository name). I’m not fond of this approach as it
|
||||||
|
means automatically checking out and merging branches which can get messy, and
|
||||||
|
also it’s weird to look at a repo’s branches page and see one branch maybe tens
|
||||||
|
or hundreds of commits both ahead and behind the default branch.
|
||||||
|
|
||||||
|
This has been made simpler and tidier now that we can use a `docs` directory
|
||||||
|
within the repository to serve content.
|
||||||
|
|
||||||
|
<img src="/images/blog/github-pages.png" alt="" class="is-centered"
|
||||||
|
style="margin-top: 20px; margin-bottom: 20px"
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
This means that I can simply re-generate the site after making changes and add
|
||||||
|
it as an additional commit to my main branch with no need to switch branches or
|
||||||
|
perform a merge.
|
||||||
|
|
||||||
|
To simplify this, I’ve added a new [publish.sh script][3] into my repository to
|
||||||
|
automate the sites. This is how it currently looks:
|
||||||
|
|
||||||
|
```language-bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SITE_ENV="prod"
|
||||||
|
|
||||||
|
# Remove the existing docs directory, build the site and create the new
|
||||||
|
# docs directory.
|
||||||
|
rm -rf ./docs
|
||||||
|
vendor/bin/sculpin generate --no-interaction --clean --env=${SITE_ENV}
|
||||||
|
touch output_${SITE_ENV}/.nojekyll
|
||||||
|
mv output_${SITE_ENV} docs
|
||||||
|
|
||||||
|
# Ensure the correct Git variables are used.
|
||||||
|
git config --local user.name 'Oliver Davies'
|
||||||
|
git config --local user.email oliver@oliverdavies.uk
|
||||||
|
|
||||||
|
# Add, commit and push the changes.
|
||||||
|
git add --all docs
|
||||||
|
git commit -m 'Build.'
|
||||||
|
git push origin HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
This begins by removing the deleting the existing `docs` directory and
|
||||||
|
re-generating the site with the specified environment. Then I add a `.nojekyll`
|
||||||
|
file and rename the output directory to replace `docs`.
|
||||||
|
|
||||||
|
Now the changes can be added, committed and pushed. Once pushed, the new code is
|
||||||
|
automatically served by GitHub Pages.
|
||||||
|
|
||||||
|
## HTTPS
|
||||||
|
|
||||||
|
GitHub Pages unfortunately does [not support HTTPS for custom domains][7].
|
||||||
|
|
||||||
|
As the site was previously using HTTPS, I didn’t want to have to go back to
|
||||||
|
HTTP, break any incoming links and lose any potential traffic. To continue using
|
||||||
|
HTTPS, I decided to [use Cloudflare][6] to serve the site via their CDN which
|
||||||
|
does allow for HTTPS traffic.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Enable automatically running `publish.sh` when new changes are pushed to
|
||||||
|
GitHub rather than running it manually. I was previously [using Jenkins][4]
|
||||||
|
and Fabric for this, though I’m also going to look into using Travis to
|
||||||
|
accomplish this.
|
||||||
|
- Add the pre-build steps such as running `composer install` and `yarn` to
|
||||||
|
install dependencies, and `gulp` to create the front-end assets. This was
|
||||||
|
previously done by Jenkins in my previous setup.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Publishing your GitHub Pages site from a /docs folder on your master
|
||||||
|
branch][2]
|
||||||
|
- [Bypassing Jekyll on GitHub Pages][5]
|
||||||
|
- [Secure and fast GitHub Pages with CloudFlare][6]
|
||||||
|
|
||||||
|
[0]: https://pages.github.com
|
||||||
|
[1]: https://github.com/opdavies/oliverdavies.uk
|
||||||
|
[2]:
|
||||||
|
https://help.github.com/articles/configuring-a-publishing-source-for-github-pages/#publishing-your-github-pages-site-from-a-docs-folder-on-your-master-branch
|
||||||
|
[3]: https://github.com/opdavies/oliverdavies.uk/blob/master/publish.sh
|
||||||
|
[4]: /articles/2015/07/21/automating-sculpin-jenkins
|
||||||
|
[5]: https://github.com/blog/572-bypassing-jekyll-on-github-pages
|
||||||
|
[6]: https://blog.cloudflare.com/secure-and-fast-github-pages-with-cloudflare
|
||||||
|
[7]: https://github.com/blog/2186-https-for-github-pages
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue