1585 lines
532 KiB
JSON
1585 lines
532 KiB
JSON
{
|
||
"articles": [
|
||
{
|
||
"title": "Testing Workshop at DrupalCamp London 2020",
|
||
"path": "/articles/drupalcamp-london-testing-workshop",
|
||
"is_draft": "false",
|
||
"created": "1580860800",
|
||
"excerpt": "This year, I\u2019m teaching a workshop at DrupalCamp London.",
|
||
"body": "<p><img src=\"\/images\/blog\/testing-workshop-drupalcamp-london\/lead.jpg\" alt=\"\" title=\"\" class=\"p-1 border\" \/><\/p>\n\n<p>This year, I\u2019m teaching a workshop at DrupalCamp London.<\/p>\n\n<p>The subject will be automated testing and test driven development in Drupal 8, and it will be on Friday 13th March 2020, between 9am and 1pm.<\/p>\n\n<p>In the workshop, I\u2019ll 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.<\/p>\n\n<p>There are also other workshops on topics including Composer, Drupal Commerce, profiling, and chatbots.<\/p>\n\n<p>For more information and to register, go to the <a href=\"https:\/\/opdavi.es\/dclondon20\" title=\"Find out more and register on the DrupalCamp London website\">DrupalCamp London website<\/a>.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupalcamp"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Using PSR-4 Autoloading for your Drupal 7 Test Cases",
|
||
"path": "/articles/psr4-autoloading-test-cases-drupal-7",
|
||
"is_draft": "false",
|
||
"created": "1580774400",
|
||
"excerpt": "How to use the PSR-4 autoloading standard for Drupal 7 Simpletest test cases.",
|
||
"body": "<p>How to use the PSR-4 autoloading standard for Drupal 7 Simpletest test cases.<\/p>\n\n<h2 id=\"the-traditional-way\">The Traditional Way<\/h2>\n\n<p>The typical way of including test cases in Drupal 7 is to add one or more classes within a <code>.test<\/code> file - e.g. <code>opdavies.test<\/code>.\nThis would typically include all of the different test cases for that module, and would be placed in the root of the module\u2019s directory alongside the <code>.info<\/code> and <code>.module<\/code> files.<\/p>\n\n<p>In order to load the files, each file would need to be declared within the <code>.info<\/code> file for the module.<\/p>\n\n<p>There is a convention that if you have multiple tests for your project, these can be split into different files and grouped within a <code>tests<\/code> directory.<\/p>\n\n<pre><code class=\"ini\">; Load a test file at the root of the module\nfiles[] = opdavies.test\n\n; Load a test file from within a subdirectory\nfiles[] = tests\/foo.test\nfiles[] = tests\/bar.test\n<\/code><\/pre>\n\n<h2 id=\"using-the-xautoload-module\">Using the xautoload Module<\/h2>\n\n<p>Whilst splitting tests into separate files makes things more organised, each file needs to be loaded separately.\nThis can be made simpler by using the <a href=\"https:\/\/www.drupal.org\/project\/xautoload\">Xautoload module<\/a>, which supports wildcards when declaring files.<\/p>\n\n<pre><code class=\"ini\">files[] = tests\/**\/*.test\n<\/code><\/pre>\n\n<p>This would load all of the <code>.test<\/code> files within the tests directory.<\/p>\n\n<h2 id=\"using-psr-4-autoloading\">Using PSR-4 Autoloading<\/h2>\n\n<p>Another option is to use PSR-4 (or PSR-0) autoloading.<\/p>\n\n<p>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 <code>.php<\/code> extension which is more standard, and the name of the file matches the name of the test class for consistency.<\/p>\n\n<p>To do this, create a <code>src\/Tests<\/code> (PSR-4) or <code>lib\/Drupal\/{module_name}\/Tests<\/code> (PSR-0) directory within your module, and then add or move your test cases there.\nAdd the appropriate namespace for your module, and ensure that <code>DrupalWebTestCase<\/code> or <code>DrupalUnitTestCase<\/code> is also namespaced.<\/p>\n\n<pre><code class=\"php\">\/\/ src\/Tests\/Functional\/OliverDaviesTest.php\n\nnamespace Drupal\\opdavies\\Tests\\Functional;\n\nclass OliverDaviesTest extends \\DrupalWebTestCase {\n \/\/ ...\n}\n<\/code><\/pre>\n\n<p>This also supports subdirectories, so you can group classes within <code>Functional<\/code> and <code>Unit<\/code> directories if you like.<\/p>\n\n<p>If you want to see an real-world example, see the Drupal 7 branch of the <a href=\"https:\/\/git.drupalcode.org\/project\/override_node_options\/tree\/7.x-1.x\">Override Node Options module<\/a>.<\/p>\n\n<h3 id=\"digging-into-the-simpletest_test_get_all-function\">Digging into the simpletest_test_get_all function<\/h3>\n\n<p>This is the code within <code>simpletest.module<\/code> that makes this work:<\/p>\n\n<pre><code class=\"php\">\/\/ simpletest_test_get_all()\n\n\/\/ ...\n\n$module_dir = DRUPAL_ROOT . '\/' . dirname($filename);\n\n\/\/ Search both the 'lib\/Drupal\/mymodule' directory (for PSR-0 classes)\n\/\/ and the 'src' directory (for PSR-4 classes).\nforeach (array(\n 'lib\/Drupal\/' . $name,\n 'src',\n) as $subdir) {\n\n \/\/ Build directory in which the test files would reside.\n $tests_dir = $module_dir . '\/' . $subdir . '\/Tests';\n\n \/\/ Scan it for test files if it exists.\n if (is_dir($tests_dir)) {\n $files = file_scan_directory($tests_dir, '\/.*\\\\.php\/');\n if (!empty($files)) {\n foreach ($files as $file) {\n\n \/\/ Convert the file name into the namespaced class name.\n $replacements = array(\n '\/' => '\\\\',\n $module_dir . '\/' => '',\n 'lib\/' => '',\n 'src\/' => 'Drupal\\\\' . $name . '\\\\',\n '.php' => '',\n );\n $classes[] = strtr($file->uri, $replacements);\n }\n }\n }\n}\n<\/code><\/pre>\n\n<p>It looks for a the tests directory (<code>src\/Tests<\/code> or <code>lib\/Drupal\/{module_name}\/Tests<\/code>) within the module, and then finds any <code>.php<\/code> files within it. It then converts the file name into the fully qualified (namespaced) class name and loads it automatically.<\/p>\n\n<h3 id=\"running-the-tests\">Running the Tests<\/h3>\n\n<p>You can still run the tests from within the Simpletest UI, or from the command line using <code>run-tests.sh<\/code>.<\/p>\n\n<p>If you want to run a specific test case using the <code>--class<\/code> option, you will now need to include the fully qualified name.<\/p>\n\n<pre><code>php scripts\/run-tests.sh --class Drupal\\\\opdavies\\\\Tests\\\\Functional\\\\OliverDaviesTest\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drupal-7"
|
||
,"testing"
|
||
,"simpletest"
|
||
,"php"
|
||
,"psr"
|
||
]
|
||
}, {
|
||
"title": "Live Blogging From SymfonyLive London 2019",
|
||
"path": "/articles/live-blogging-symfonylive-london",
|
||
"is_draft": "false",
|
||
"created": "1568332800",
|
||
"excerpt": "",
|
||
"body": "<p>Inspired by <a href=\"https:\/\/twitter.com\/stauffermatt\">Matt Stauffer<\/a>'s <a href=\"https:\/\/mattstauffer.com\/blog\/introducing-laravel-vapor\">live blogging of the keynote<\/a> at Laracon US, I\u2019m going to do the same for the sessions that I\u2019m attending at <a href=\"https:\/\/london2019.live.symfony.com\">SymfonyLive London 2019<\/a>...<\/p>\n\n<h2 id=\"keynote-back-to-the-basics\">Keynote (Back to the basics)<\/h2>\n\n<p><strong>Embrace the Linux philosophy<\/strong><\/p>\n\n<ul>\n<li>How we grow the Symfony ecosystem. Built abstracts.<\/li>\n<li>HttpFoundation, HttpKernel<\/li>\n<li>Moved to infrastructure<\/li>\n<li>A few abstractions on top of PHP. Improved versions of PHP functions (<code>dump<\/code> and <code>var_dump<\/code>)<\/li>\n<li>Started a add higher level abstractions (e.g. Mailer), built on the lower ones.<\/li>\n<li>Recently worked on PHPUnit assertions. Mailer in Symony 4.4. Can test if an email is sent or queued<\/li>\n<\/ul>\n\n<p><strong>Building flexible high-level abstractions on top of low-level ones<\/strong><\/p>\n\n<h3 id=\"what%27s-next%3F\">What's next?<\/h3>\n\n<ul>\n<li>Mailer announced in London last year. New component.<\/li>\n<li>System emails? e.g. new customer, new invoice.<\/li>\n<li>Symfony Mailer = Built-in responsive, flexible, and generic system emails\n\n<ul>\n<li>Twig with TwigExtraBundle<\/li>\n<li>Twig <code>inky-extra<\/code> package (Twig 1.12+)<\/li>\n<li>Zurb Foundation for Emails CSS stylesheet<\/li>\n<li>Twig <code>cssinliner-extra<\/code> package (Twig 1.12+)<\/li>\n<li>Optimised Twig layouts<\/li>\n<\/ul><\/li>\n<li><code>SystemEmail<\/code> class extends templated email<\/li>\n<li>Can set importance,<\/li>\n<li>Customisable<\/li>\n<li>Always trying to keep flexible, so things can be overidden and customised<\/li>\n<\/ul>\n\n<h3 id=\"sending-sms-messages\">Sending SMS messages<\/h3>\n\n<ul>\n<li>new <code>Texter<\/code> and <code>SmsMessage<\/code> class for sending SMS messages<\/li>\n<li>Same abstraction as emails, but for SMS messages<\/li>\n<li>Based on HttpClient + Symfony Messenger and third-party providers (Twilio and Nexmo) <code>twilio:\/\/<\/code> and <code>nemxo:\/\/<\/code><\/li>\n<li>Can set via transport <code>$sms->setTransport('nexmo')<\/code><\/li>\n<li>Extend the <code>SystemEmail<\/code> and do what you want<\/li>\n<li>Failover<\/li>\n<\/ul>\n\n<h3 id=\"sending-messages\">Sending Messages<\/h3>\n\n<ul>\n<li>Create <code>ChatMessage<\/code><\/li>\n<li>Telegram and Slack<\/li>\n<li><code>$message->setTransport('telegram')<\/code>, <code>$bus->dispatch($message)<\/code><\/li>\n<li>Send to Slack <strong>and<\/strong> Telegram<\/li>\n<li><code>SlackOptions<\/code> and <code>TelegramOptions<\/code> for adding emojis etc<\/li>\n<li>Common transport layer <code>TransportInterface<\/code>, <code>MessageInterface<\/code><\/li>\n<li>Failover - e.g. if Twilio is down, send to Telegram<\/li>\n<\/ul>\n\n<h3 id=\"new-component---symfonynotifier\">New component - SymfonyNotifier<\/h3>\n\n<ul>\n<li>Channels - email, SMS, chat<\/li>\n<li>Transport, slack, telegram, twilio<\/li>\n<li>Create a notification, arguments are message and transports (array)<\/li>\n<li>Receiver<\/li>\n<li>Customise notifications, <code>InvoiceNotification<\/code> extends <code>Notification<\/code>. <code>getChannels<\/code>\n\n<ul>\n<li>Override default rendering<\/li>\n<li><code>ChatNotificationInterface<\/code> - <code>asChatMessage()<\/code><\/li>\n<\/ul><\/li>\n<li>Semantic configuration\n\n<ul>\n<li><code>composer req twilio-notifier telegram-notifier<\/code><\/li>\n<\/ul><\/li>\n<li>Channels\n\n<ul>\n<li>Mailer<\/li>\n<li>Chatter<\/li>\n<li>Texter<\/li>\n<li>Browser<\/li>\n<li>Pusher (iOS, Android, Desktop native notifications)<\/li>\n<li>Database (web notification centre)<\/li>\n<li><strong>A unified way to notify Users via a unified Transport layer<\/strong><\/li>\n<\/ul><\/li>\n<li>Each integration is only 40 lines of code<\/li>\n<\/ul>\n\n<h3 id=\"what-about-a-systemnotification%3F\">What about a SystemNotification?<\/h3>\n\n<ul>\n<li>Autoconfigured channels<\/li>\n<li><code>new SystemNotification<\/code>, <code>Notifier::getSystemReceivers<\/code><\/li>\n<li>Importance, automatically configures channels<\/li>\n<li>Different channels based on importance<\/li>\n<li><code>ExceptionNotification<\/code> - get email with stack trace attached<\/li>\n<\/ul>\n\n<p>Notifier\n* send messages via a unified api\n* send to one or many receivers\n* Default configu or custom one<\/p>\n\n<h3 id=\"%E2%A0how-can-we-leverage-this-new-infrastructure%3F\">\u00a0How can we leverage this new infrastructure?<\/h3>\n\n<ul>\n<li><code>Monolog NotifierHandler<\/code> - triggered on <code>Error<\/code> level logs<\/li>\n<li>Uses notified channel configuration<\/li>\n<li>Converts Error level logs to importance levels<\/li>\n<li>Configurablelike other Notifications<\/li>\n<li>40 lines of code<\/li>\n<li><p>Failed Messages Listener - 10 lines of glue code<\/p><\/li>\n<li><p><strong>Experimental component in 5.0<\/strong><\/p><\/li>\n<li>Can't in in 4.4 as it's a LTS version<\/li>\n<li>First time an experimental component is added<\/li>\n<li>Stable in 5.1<\/li>\n<\/ul>\n\n<h2 id=\"queues%2C-busses-and-the-messenger-component-tobias-nyholm\">Queues, busses and the Messenger component (Tobias Nyholm)<\/h2>\n\n<ul>\n<li>Stack is top and buttom - Last-in, first-out<\/li>\n<li>Queue is back and front - last in, first out<\/li>\n<\/ul>\n\n<h3 id=\"2013\">2013<\/h3>\n\n<ul>\n<li>Using Symfony, used 40 or 50 bundles in a project - too much information!<\/li>\n<li>Used to copy and paste, duplicate a lot of code<\/li>\n<li>Testing your controllers - controllers as services?<\/li>\n<li>Controllers are 'comfortable'<\/li>\n<li>Tried adding <code>CurrentUserProvider<\/code> service to core, should be passed as an argument. Cannot test.<\/li>\n<li>'Having Symfony all over the place wasn't the best thing' - when to framework (Matthias Noback)\n\n<ul>\n<li>Hexagonal architecture<\/li>\n<li>Keep your kernel away from infrastructure. Let the framework handle the infrastructure.<\/li>\n<\/ul><\/li>\n<li>Controller -> Command -> Command Bus -> <code>CommandHandler<\/code><\/li>\n<\/ul>\n\n<h4 id=\"what-did-we-win%3F\">What did we win?<\/h4>\n\n<ul>\n<li>Can leverage Middleware with a command bus<\/li>\n<li>Queues as a service (RabbitMQ)<\/li>\n<li>Work queue - one producer, multiple consumers<\/li>\n<li>Queues should be durable - messages are also stored on disk, consumers should acknowledge a message once a message is handled<\/li>\n<li>Publish\/subscribe\n\n<ul>\n<li>Producer -> Fanout\/direct with routing (multiple queues) -> multiple consumers<\/li>\n<\/ul><\/li>\n<li>Topics - wildcards<\/li>\n<\/ul>\n\n<h3 id=\"2016\">2016<\/h3>\n\n<ul>\n<li>New intern. Understand everything, 'just PHP'. Plain PHP application, not 'scary Symfony'<\/li>\n<\/ul>\n\n<h3 id=\"symfony-messenger\">Symfony Messenger<\/h3>\n\n<ul>\n<li><code>composer req symfony\/messager<\/code> - best MessageBus implementation<\/li>\n<li>Message -> Message bus -> Message handler<\/li>\n<li>Message is a plain PHP class<\/li>\n<li>Handler is a normal PHP class which is invokable<\/li>\n<li><code>messenger:message_hander<\/code> tag in config<\/li>\n<li>Autowire with <code>MessageHandlerInterface<\/code><\/li>\n<li>What if it takes 20 seconds to send a message? Use asynchronous.<\/li>\n<li>Transports as middleware (needs sender, receiver, configurable with DSN, encode\/decode). <code>MESSENGER_DSN<\/code> added to <code>.env<\/code><\/li>\n<li>Start consumer with <code>bin\/console messager:consume-messages<\/code>. Time limit with <code>--time-limit 300<\/code><\/li>\n<li>PHP Enqueue - production ready, battle-tested messaging solution for PHP<\/li>\n<\/ul>\n\n<h3 id=\"issues\">Issues<\/h3>\n\n<ul>\n<li>Transformers, takes an object and transforms into an array - <code>FooTransformer implements TransformerInterface<\/code>.<\/li>\n<li>Don't break other apps by changing the payload.<\/li>\n<\/ul>\n\n<h4 id=\"multiple-buses\">Multiple buses<\/h4>\n\n<ul>\n<li>Command bus, query bus, event bus<\/li>\n<li>Separate actions from reactions<\/li>\n<\/ul>\n\n<h4 id=\"envelope\">Envelope<\/h4>\n\n<ul>\n<li>Stamps for metadata - has the item been on the queue already?<\/li>\n<\/ul>\n\n<h4 id=\"failures\">Failures<\/h4>\n\n<ul>\n<li>Requeue, different queue or same queue after a period of time<\/li>\n<li>Failed queue 1 every minute, failed queue 2 every hour - temporary glitches or a bug?<\/li>\n<\/ul>\n\n<h4 id=\"creating-entities\">Creating entities<\/h4>\n\n<ul>\n<li>What if two users registered at the same tiem? Use uuids rather than IDs.<\/li>\n<li><p>Symfony validation - can be used on messages, not just forms.<\/p><\/li>\n<li><p>Cache everything<\/p>\n\n<ul>\n<li>Option 1: HTTP request -> Thin app (gets responses from Redis) -> POST to queue. Every GET request would warm cache<\/li>\n<li>Option 2: HTTP request -> Thin app -> return 200 response -> pass to workers<\/li>\n<\/ul><\/li>\n<li><p>Tip: put Command and CommandHandlers in the same directory<\/p><\/li>\n<\/ul>\n\n<h2 id=\"%E2%A0httpclient-nicolas-grekas\">\u00a0HttpClient (Nicolas Grekas)<\/h2>\n\n<ul>\n<li>new symfony component, released in may<\/li>\n<li>Httpclient contracts, separate package that contains interfaces\n\n<ul>\n<li>Symfony<\/li>\n<li>PHP-FIG<\/li>\n<li>Httplug<\/li>\n<\/ul><\/li>\n<li><code>HttpClient::create()<\/code>. <code>$client->get()<\/code><\/li>\n<li>JSON decoded with error handling<\/li>\n<li>Used on symfony.com website (#1391). Replaces Guzzle <code>Client<\/code> for <code>HttpClientInterface<\/code><\/li>\n<li>Object is stateless, Guzzle is not. Doesn't handle cookies, cookies are state<\/li>\n<li>Remove boilerplate - use <code>toArray()<\/code><\/li>\n<li>Options as third argument - array of headers, similar to Guzzle<\/li>\n<\/ul>\n\n<h3 id=\"what-can-we-do-with-the-response%3F\">What can we do with the Response?<\/h3>\n\n<ul>\n<li><code>getStatusCode(): int<\/code><\/li>\n<li><code>getHeaders(): array<\/code><\/li>\n<li><code>getContent(): string<\/code><\/li>\n<li><code>toArray(): array<\/code><\/li>\n<li><code>cancel(): void<\/code><\/li>\n<li><code>getInfo(): array<\/code> - metadata<\/li>\n<li>Everything is lazy!<\/li>\n<li>80% of use-cases covered<\/li>\n<\/ul>\n\n<h3 id=\"what-about-psr-18%3F\">What about PSR-18?<\/h3>\n\n<ul>\n<li>Decorator\/adapter to change to PSR compatible<\/li>\n<li>Same for Httplug<\/li>\n<\/ul>\n\n<h3 id=\"what-about-the-remaining-20%25%3F\">What about the remaining 20%?<\/h3>\n\n<ul>\n<li>Options are part of the abstraction, not the implementation<\/li>\n<\/ul>\n\n<h4 id=\"some-of-the-options\">Some of the options<\/h4>\n\n<ul>\n<li><code>timeout<\/code> - control inactivity periods<\/li>\n<li><code>proxy<\/code> - get through a http proxy<\/li>\n<li><code>on_progress<\/code> - display a progress bar \/ build a scoped client<\/li>\n<li><code>base_url<\/code> - resolve relative URLS \/ build a scoped client<\/li>\n<li><code>resolve<\/code> - protect webhooks against calls to internal endpoints<\/li>\n<li><p><code>max_redirects<\/code> - disable or limit redirects<\/p><\/li>\n<li><p>Robust and failsafe by default<\/p><\/li>\n<li><p>Streamable uploads - <code>$mimeParts->toIterable()<\/code>.<\/p><\/li>\n<li><p>donwload a file<\/p>\n\n<pre><code class=\"php\">foreach ($client->stream($response) as $chunk) {\n \/\/ ... \n}\n<\/code><\/pre><\/li>\n<li><p>Responses are lazy, requests are concurrent<\/p><\/li>\n<li>Asychronus requests. Reading in network order<\/li>\n<\/ul>\n\n<pre><code>foreach ($client->stream($responses) as $response => $chunk) {\n if ($chunk->isLast()) {\n \/\/ a $response completed\n } else {\n \/\/ a $response's got network activity or timeout\n }\n}\n<\/code><\/pre>\n\n<ul>\n<li>379 request completed in 0.4s!<\/li>\n<li><code>Stream<\/code> has second argument, max number of seconds to wait before yielding a timeout chunk<\/li>\n<li><code>ResponseInterface::getInfo()<\/code> - get response headers, redirect count and URL, start time, HTTP method and code, user data and URL\n\n<ul>\n<li><code>getInfo('debug')<\/code> - displays debug information<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"the-components\">The components<\/h3>\n\n<ul>\n<li><code>NativeHttpClient<\/code> and <code>CurlHttpClient<\/code>\n\n<ul>\n<li>both provide\n\n<ul>\n<li>100% contracts<\/li>\n<li>secure directs<\/li>\n<li>extended (time) info<\/li>\n<li>transparent HTTP compression and (de)chunking<\/li>\n<li>automatic HTTP proxy configuration via env vars<\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h4 id=\"%60nativehttpclient%60\"><code>NativeHttpClient<\/code><\/h4>\n\n<ul>\n<li>is most portable, works for everyone<\/li>\n<li>based on HTTP stream wrapper with fixed redirect logic<\/li>\n<li>blocking until response headers arrive<\/li>\n<\/ul>\n\n<h4 id=\"%60curlhttpclient%60\"><code>CurlHttpClient<\/code><\/h4>\n\n<ul>\n<li>Requires ext-curl with fixed redirection logic<\/li>\n<li>Multiplexing response headers and bodies<\/li>\n<li>Leverages HTTP\/2 and PUSH when available<\/li>\n<li>Keeps connections open also between synchronous requests, no DNS resolution so things are faster<\/li>\n<\/ul>\n\n<h4 id=\"decorators\">Decorators<\/h4>\n\n<ul>\n<li>ScopingHttpClient - auto-configure options based on request URL<\/li>\n<li>MockHttpClient - for testing, doesn't make actual HTTP requests<\/li>\n<li>CachingHttpClient - adds caching on a HTTP request<\/li>\n<li>Psr18Client<\/li>\n<li>HttplugClient<\/li>\n<li>TraceableHttpClient<\/li>\n<\/ul>\n\n<h3 id=\"combining\">Combining<\/h3>\n\n<h4 id=\"frameworkbundle%2Fautowiring\">FrameworkBundle\/Autowiring<\/h4>\n\n<pre><code class=\"yml\">framework:\n http_client:\n max_host_connections: 4\n deault_options:\n # ....\n scoped_client:\n # ...\n<\/code><\/pre>\n\n<h4 id=\"httpbrowser\">HttpBrowser<\/h4>\n\n<ul>\n<li>HttpClient + DomCrawler + CssSelector + HttpKernel + BrowserKit<\/li>\n<li>RIP Goutte!<\/li>\n<\/ul>\n\n<h3 id=\"coming-in-4.4...\">Coming in 4.4...<\/h3>\n\n<ul>\n<li><code>max_duration<\/code><\/li>\n<li><code>buffer<\/code> based on a callable<\/li>\n<li><code>$chunk->isInformational()<\/code><\/li>\n<li><code>$response->toStream()<\/code><\/li>\n<li>Async-compatible extensibility, when decoration is not enough<\/li>\n<\/ul>\n\n<p><code>composer req symfony\/http-client<\/code><\/p>\n\n<h2 id=\"symfony-checker-is-coming-valentine-boineau\">Symfony Checker is coming (Valentine Boineau)<\/h2>\n\n<ul>\n<li>Static analysis tool for Symfony\n\n<ul>\n<li>Does a method exist?<\/li>\n<li>Is it deprecated?<\/li>\n<\/ul><\/li>\n<li>insight.symfony.com<\/li>\n<li>@symfonyinsight<\/li>\n<li>Released soon<\/li>\n<\/ul>\n\n<h3 id=\"%E2%A0differences\">\u00a0Differences<\/h3>\n\n<ul>\n<li>Specialise in Symfony - can see more relevant things<\/li>\n<li>Different interface to other services<\/li>\n<\/ul>\n\n<h2 id=\"feeling-unfulfilled-by-spa-promises%3F-go-back-to-twig-dan-blows\">Feeling unfulfilled by SPA promises? Go back to Twig (Dan Blows)<\/h2>\n\n<p>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<\/p>\n\n<h3 id=\"why-spas%3F\">Why SPAs?<\/h3>\n\n<ul>\n<li>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<\/li>\n<li>no full page refresh<\/li>\n<li>Supposed to be much quicker<\/li>\n<li>'Right tool for the job' - JS on the front-end, PHP on the back-end<\/li>\n<li>Division of responsibility == faster development<\/li>\n<li>Reusable API - Api -> Mobile App and SPA - easy to add another consumer<\/li>\n<li>Easier to debug?<\/li>\n<\/ul>\n\n<h3 id=\"why-not-spas%3F\">Why not SPAs?<\/h3>\n\n<ul>\n<li>Lots of HTTP requests (400 to load the initial page on one project) == slow front end<\/li>\n<li>Blurred responsibilities == tightly coupled teams<\/li>\n<li>harder to debug, bugs fall between systems and teams. Huge gap between front-end and back-end, passing responsibilites.<\/li>\n<li>You can fix these problems in SPAs, but is it worth it?\n\n<ul>\n<li>Examples of good SPAs - Trello, Flickr<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"using-twig-as-an-alternative-to-an-spa%3F\">Using Twig as an alternative to an SPA?<\/h3>\n\n<h4 id=\"faster-ui---try-and-figure-out-where-the-problem-is.\">Faster UI - Try and figure out where the problem is.<\/h4>\n\n<p>If you're trying to speed things up, find out where the problem is.<\/p>\n\n<ul>\n<li>Browser tools<\/li>\n<li>Web Debug Toolbar<\/li>\n<li>Blackfire<\/li>\n<li>Optimise and monitor<\/li>\n<\/ul>\n\n<h4 id=\"speed-up-twig\">Speed up Twig<\/h4>\n\n<ul>\n<li>Speeding up Symfony<\/li>\n<li>ext\/twig (PHP5 only, not PHP 7)<\/li>\n<li>Store compiled templates in Opcache, make sure it's enabled<\/li>\n<li>Render assets though the webserver (assetic not running all the time)<\/li>\n<\/ul>\n\n<h4 id=\"edge-side-includes\">Edge side includes<\/h4>\n\n<ul>\n<li>Component cached differently to the rest of the page<\/li>\n<li>Varnish\/Nginx<\/li>\n<li><code>render_esi<\/code><\/li>\n<li>News block that caches frequently, rest of the page<\/li>\n<\/ul>\n\n<h4 id=\"http%2F2-with-weblink\">HTTP\/2 with Weblink<\/h4>\n\n<ul>\n<li>slow finding CSS files to load - 'push' over CSS files, doesn't need to wait<\/li>\n<li><code>preload()<\/code> - https:\/\/symfony.com\/doc\/current\/web_link.html<\/li>\n<\/ul>\n\n<h4 id=\"live-updating-pages\">Live updating pages<\/h4>\n\n<ul>\n<li>Instantly update when sports results are updated, news articles are added<\/li>\n<li>Mercure - https:\/\/github.com\/symfony\/mercure<\/li>\n<li>LiveTwig - whole block or whole section, and live update <code>render_live<\/code><\/li>\n<li>Turbolinks - replace whole body, keeps CSS and JS in memory. Merges new stuff in. <code>helthe\/turbolinks<\/code><\/li>\n<li>ReactPHP - shares kernel between requests<\/li>\n<\/ul>\n\n<h3 id=\"writing-better-code-with-twig\">Writing better code with Twig<\/h3>\n\n<ul>\n<li>Keep templates simple. Avoid spaghetti code, only about UI. HTML or small amounts of Twig.<\/li>\n<li>Avoid delimeter chains\n\n<ul>\n<li>Bad:<code>blog_post.authors.first.user_account.email_address<\/code><\/li>\n<li>Good <code><\/code><\/li>\n<li>Less brittle, slow<\/li>\n<\/ul><\/li>\n<li>Filters\n\n<ul>\n<li>Use filters to be precise<\/li>\n<li>Custom filters<\/li>\n<li>Avoid chains. Can cause odd results. Create a new filter in PHP<\/li>\n<\/ul><\/li>\n<li>Functions\n\n<ul>\n<li>Write your own functions<\/li>\n<li>Simpler templates<\/li>\n<li>Get data, can use boolean statements<\/li>\n<\/ul><\/li>\n<li><p>Components<\/p>\n\n<ul>\n<li>Break a page into components rather than one large page<\/li>\n<li><code>include()<\/code><\/li>\n<li>Use <code>only<\/code> to only pass that data. less tightenly coupled.<\/li>\n<li><code>render<\/code> calls the whole of Symfony, boots Kernel, can be expensive and slow<\/li>\n<li>Loosely couple templates and controllers\n\n<ul>\n<li>Keep responses simple<\/li>\n<li>What makes sense<\/li>\n<li>if you need extra data in the template, get it in the template<\/li>\n<\/ul><\/li>\n<li>View models\n\n<ul>\n<li>Mixed results<\/li>\n<li><code>BlogPostViewModel<\/code><\/li>\n<li>Can result in boilerplate code<\/li>\n<li>Can be useful if the view model is different to the Entity<\/li>\n<\/ul><\/li>\n<li>DRY\n\n<ul>\n<li>\"Don't repeat yourself\"<\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<li><p>Faster development<\/p>\n\n<ul>\n<li>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.<\/li>\n<\/ul><\/li>\n<li>Help your front end\n\n<ul>\n<li>Webpack - Encore<\/li>\n<li>Type hinting in functions and filters, easier to debug<\/li>\n<li>Logging<\/li>\n<li>Friendly exceptions - help front-end devs by returning meaningful, readbale errors<\/li>\n<li>Web Debug Toolbar and Profiler, provide training for toolbar and profilers<\/li>\n<li>Twig-friendly development environment - Twig support in IDEs and text editors<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<p>SPAs are sometimes teh right solution. Why do they want to use it, can the same benefits be added with Twig?<\/p>\n\n<p>3 most important points:<\/p>\n\n<ul>\n<li>Profile, identidy, optimise, monitor<\/li>\n<li>Loosely couple templates to your app code<\/li>\n<li>Help your front ends - put your front end developers first<\/li>\n<li>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.<\/li>\n<\/ul>\n\n<h2 id=\"bdd-your-symfony-application-kamil-kokot\">BDD Your Symfony Application (Kamil Kokot)<\/h2>\n\n<ul>\n<li>Applying BDD to Sylius<\/li>\n<li>2 years since release of Sylius (Symfony 2 alpha)<\/li>\n<li>The business part is more important than the code part<\/li>\n<\/ul>\n\n<h3 id=\"what-is-bdd%3F\">What is BDD?<\/h3>\n\n<ul>\n<li>Behaviour driven development. Combines TDD and DDD, into an agile methodology<\/li>\n<li>Encourages communication and creates shared understanding<\/li>\n<li>Living, executable documentation that non-programmers understand. Always correct.<\/li>\n<li>Feature file\n\n<ul>\n<li>Feature<\/li>\n<li>Scenario - example of the behaviour for this feature. Simple, atomic. (e.g. I need a product in order to add it to a cart)<\/li>\n<li>In order to...<\/li>\n<li>Who gets the benefit?<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"%E2%A0bdd-in-practice\">\u00a0BDD in practice<\/h3>\n\n<ul>\n<li>Feature: booking flight tickets<\/li>\n<li>Scenario: booking flight ticket for one person\n\n<ul>\n<li>Given there are the following flights...<\/li>\n<li>When I visit '\/flight\/LTN-WAW'<\/li>\n<li>Then I should be on '\/flight\/LTN-WAW'<\/li>\n<li>Add I should see \"Your flight has been booked.\" in \"#result\"<\/li>\n<\/ul><\/li>\n<li>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?\n\n<ul>\n<li>We just need to know that there are 5 seats left on a flight<\/li>\n<li>Talk and communicate about how the feature is going to work - not just developers<\/li>\n<li>BDD aids communication<\/li>\n<\/ul><\/li>\n<li>Questions we can ask\n\n<ul>\n<li>Can we get a different outcome when the context changes?\n\n<ul>\n<li>When there was only one seat available<\/li>\n<li>When there were no available seats<\/li>\n<\/ul><\/li>\n<li>Can we get the same outcome when the event changes? Can we change 'When' and 'Then stays the same'\n\n<ul>\n<li>When it is booked for an adult and a child<\/li>\n<li>When it is booked for an adult<\/li>\n<\/ul><\/li>\n<li>Does anything else happen that is not mentioned?\n\n<ul>\n<li>Generate an invoice if a seat is booked<\/li>\n<li>a pilot would like to get a notification that a seat was booked.<\/li>\n<\/ul><\/li>\n<li>Figuring out the rules\n\n<ul>\n<li>Adults are 15+ years old<\/li>\n<li>Children are 2-14 years old<\/li>\n<li>Infants and children can only travel with an adult<\/li>\n<li>We don't allow for overbooking<\/li>\n<\/ul><\/li>\n<li>Translating rules into examples\n\n<ul>\n<li>Add a new scenario for each rule - e.g. don't allow over booking\n\n<ul>\n<li>\"And the flight should be no longer available...\"<\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"behat\">Behat<\/h3>\n\n<ul>\n<li>Used to automate and execute BDD tests, also SpecDDD<\/li>\n<li>maps steps to PHP code<\/li>\n<li>Given a context, when an event, then an outcome<\/li>\n<li>Domain Context, API context<\/li>\n<li>class implements <code>Context<\/code>, annotations for <code>@Given<\/code>, <code>@When<\/code>, <code>@Then<\/code>. allows for arguments and regular expressions<\/li>\n<li>Suites: change what code is executed, and what scenarios are executed. context and tags<\/li>\n<li>FriendsOfBehat SymfonyExtension - integrates Behat with Symfony\n\n<ul>\n<li>Contexts registered as Symfony services - inject dependencies, service as a context in Behat. Need to be 'public' for it to work<\/li>\n<li>Reduces boilerplate code. Supports autowiring.<\/li>\n<li>Zero configuration<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"domain-context\">Domain context<\/h3>\n\n<ul>\n<li><code>Given<\/code> verb matches <code>@Given<\/code> annotation. Same for <code>When<\/code> and <code>Then<\/code>.<\/li>\n<li>Transformers, type hint name string, return Client instance<\/li>\n<\/ul>\n\n<h3 id=\"api-context\">API context<\/h3>\n\n<ul>\n<li>inject <code>FlightBookingService<\/code> and <code>KernelBrowser<\/code><\/li>\n<li>Use <code>$this->kernelBrowser->request()<\/code><\/li>\n<li>Use <code>assert()<\/code> function wuthin <code>@Then<\/code><\/li>\n<\/ul>\n\n<h3 id=\"back-to-reality---how-it%27s-done-with-sylius\">Back to reality - how it's done with Sylius<\/h3>\n\n<ul>\n<li>Business part applies to all context. Start talking about what needs to be done, start communicating<\/li>\n<li>Implement contexts for UI and API<\/li>\n<li>12716 steps, 1175 scenarios, 8 min 8 sec, 2.4 scenarios \/sec<\/li>\n<li>12x faster than JS (17 min 48 sec, 0.19 scenario \/ sec)<\/li>\n<li><p>Treat test CI environment like production<\/p>\n\n<ul>\n<li>Turn off debug settings, add caching<\/li>\n<li>Enable OPcache<\/li>\n<\/ul><\/li>\n<li><p>Write features in a natural way<\/p><\/li>\n<li>Too many setup steps - merge steps. less visual debt. e.g. Create currency, zone and locale when creating a store<\/li>\n<li>Avoid scenarios that are too detailed. You should specify only what's important to this scenario.<\/li>\n<\/ul>\n\n<h2 id=\"migrating-to-symfony-one-route-at-a-time-steve-winter\">Migrating to Symfony one route at a time (Steve Winter)<\/h2>\n\n<ul>\n<li><p>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.<\/p><\/li>\n<li><p>Subscription based index of suppliers<\/p><\/li>\n<li>New requirements to implement by the client<\/li>\n<li>Our requirements: Needed a deployment process, make it testable, fix the build chain<\/li>\n<li>Solution attempt 1: Migrate to a new version of the current framework\n\n<ul>\n<li>Minor template and design changes were fine<\/li>\n<li>Modifiy features, add new dependencies.<\/li>\n<\/ul><\/li>\n<li>Solution attempt 2: Upgrade to the latest version - same outcome due to multiple BC breaks (no semver), lots of manual steps<\/li>\n<li>Solution attempt 3: Symfony!\n\n<ul>\n<li>Semver! Backwards compatibility promise<\/li>\n<li>Symfony app to run in parallel, Apache proxy rules and minor changes to the legacy app, added data transfer mechanisms<\/li>\n<li>Anything new done in Symfony<\/li>\n<li>Installed on the same server with it's own vhost but not publicly accessible<\/li>\n<li>Deployed independently of legacy app<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"apache-proxy-rules\">Apache proxy rules<\/h3>\n\n<p>Proxy <code>\/public<\/code> to symfony app<\/p>\n\n<h3 id=\"legacy-app\">Legacy app<\/h3>\n\n<ul>\n<li>Shared cookie for single login between apps - user account details (name etc), session details (login time)<\/li>\n<\/ul>\n\n<h3 id=\"added-functionality\">Added functionality<\/h3>\n\n<ul>\n<li>Built in Symfony<\/li>\n<li>new proxy rules for new routes<\/li>\n<li>Add menu links to legacy app menu<\/li>\n<li>How do we show how many reminders are active?\n\n<ul>\n<li>Symfony based API called from the front-end<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"migrating-routes\">Migrating routes<\/h3>\n\n<ul>\n<li>Rebuilt or extend in Symfony app<\/li>\n<li>Test and deploy, then update the apache config to add new proxy rules<\/li>\n<\/ul>\n\n<h3 id=\"a-gotcha\">A gotcha<\/h3>\n\n<ul>\n<li>Legacy app uses CSRF<\/li>\n<li>Needed to track the token, added to shared cookie and pass through to the Symfony side<\/li>\n<\/ul>\n\n<h3 id=\"storing-data\">Storing data<\/h3>\n\n<ul>\n<li>Both apps using the same data with different credentials<\/li>\n<li>Some shared tables, some tables are specific to each app<\/li>\n<\/ul>\n\n<h3 id=\"remaining-challenges\">Remaining challenges<\/h3>\n\n<ul>\n<li>User session management, still handled by legacy app<\/li>\n<li>Templating\/CSS - two versions of everything\n\n<ul>\n<li>Next step: move all CSS to Symfony<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h3 id=\"summary\">Summary<\/h3>\n\n<ul>\n<li>Add Symfony app, Apache proxy rules for routes<\/li>\n<li>User transfer mechanisms<\/li>\n<li>New functionality added in Symfony<\/li>\n<\/ul>\n\n<h3 id=\"is-this-right-for-you%3F\">Is this right for you?<\/h3>\n\n<p>It depends. Fine for a 'modest' size. Use a real proxy for larger scale apps, use different servers with database replication.<\/p>\n\n<h2 id=\"closing-keynote%3A-the-fabulous-world-of-emojis-and-other-unicode-symbols-nicolas-grekas\">Closing Keynote: The fabulous World of Emojis and other Unicode symbols (Nicolas Grekas)<\/h2>\n\n<ul>\n<li>ASCII. Still used today. Map between the first 128 numbers to characters. OK for UK and US.<\/li>\n<li>256 numbers in Windows-1252 (character sets). Each country had their own set.<\/li>\n<li>It's legacy. 0.2% for Windows-1252. 88.8% for UTF-8 (Feb 2017)<\/li>\n<li>Unicode: 130k characters, 135 scripts (alphabets)<\/li>\n<li>Validation errors using native alphabet - e.g. invalid last name when submitting a form<\/li>\n<li>17 plans, each square is 255 code points<\/li>\n<li>Emojis are characters, not images<\/li>\n<li>Gliph is a visual representation of a character<\/li>\n<li>From code points to bytes\n\n<ul>\n<li>UTF-8: 1,2,3 or 4 bytes<\/li>\n<li>UTF16: 2 or 4 bytes<\/li>\n<li>UTF-32: 4 bytes<\/li>\n<\/ul><\/li>\n<li>UTF-8 is compatible with ASCII<\/li>\n<li>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.<\/li>\n<li>Collations - ordering is depends on the language. 'ch' in Spanish is a single character.<\/li>\n<li>Single number in unicode to represent accents. Combining characters.<\/li>\n<li>Composed (NFC) and decomposed (NFD) forms - normalisation for comparison<\/li>\n<li>Grapheme clusters - multiple characters, but one letter as you write it (separate characters for letters and accent)<\/li>\n<li>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.<\/li>\n<\/ul>\n\n<h3 id=\"unicode-fundamentals\">unicode fundamentals<\/h3>\n\n<ul>\n<li>uppercase, lowercase, folding<\/li>\n<li>compositions, ligatures<\/li>\n<li>comparistions - normalisations and collations<\/li>\n<li>segmentation: characters, words, sentences and hyphens<\/li>\n<li>locales: cultural conventions, translitterations<\/li>\n<li>identifiers & security, confusables<\/li>\n<li>display: direction, width<\/li>\n<\/ul>\n\n<h3 id=\"unicode-in-practice\">unicode in practice<\/h3>\n\n<ul>\n<li>MySQL - <code>utf*_*<\/code>. <code>SET NAMES utf8mb4<\/code> for security and storing emojis. Cannot store emojis with <code>utf8<\/code><\/li>\n<\/ul>\n\n<h3 id=\"in-php\">in php<\/h3>\n\n<ul>\n<li><code>mb_*()<\/code><\/li>\n<li><code>iconv_*()<\/code><\/li>\n<li><code>preg_*()<\/code><\/li>\n<li><code>grapheme_*()<\/code> <code>normalizer_*()<\/code><\/li>\n<li><code>symfony\/polyfill-*<\/code> - pure PHP implementation<\/li>\n<li>Made a component - <strong>symfony\/string<\/strong> - https:\/\/github.com\/symfony\/symfony\/pull\/33553<\/li>\n<li>Object orientated api for strings. Immutable value objects<\/li>\n<li><code>AbstractString<\/code>\n\n<ul>\n<li><code>GraphemeString<\/code><\/li>\n<li><code>Utf8String<\/code><\/li>\n<li><code>BinaryString<\/code><\/li>\n<\/ul><\/li>\n<li>AbstractString - Methods to serialize, get length, to binary or grapheme or utf8\n\n<ul>\n<li>Methods for starts with, ends with, is empty, join, prepend, split, trim, title etc<\/li>\n<\/ul><\/li>\n<\/ul>\n",
|
||
"tags": ["conference"
|
||
,"symfony"
|
||
,"symfonylive"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Speaking at DrupalCon Amsterdam",
|
||
"path": "/articles/speaking-drupalcon-amsterdam",
|
||
"is_draft": "false",
|
||
"created": "1564012800",
|
||
"excerpt": "I\u2019m going to be attending DrupalCon Europe again this year, but for the first time as a speaker.",
|
||
"body": "<p class=\"lead\">I\u2019ve attended numerous <a href=\"http:\/\/drupalcon.net\">DrupalCons<\/a> since my first in Prague in 2013, as a delegate, as a <a href=\"https:\/\/www.drupal.org\/association\">Drupal Association<\/a> staff member, and also as a contribution sprint mentor. I\u2019m excited to be attending DrupalCon Amsterdam again this year - but as my first time as a DrupalCon speaker.<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<p lang=\"en\" dir=\"ltr\">Super excited to be giving my first <a href=\"https:\/\/twitter.com\/DrupalConEur?ref_src=twsrc%5Etfw\">@DrupalConEur<\/a> talk!<a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupalcon?src=hash&ref_src=twsrc%5Etfw\">#drupalcon<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/php?src=hash&ref_src=twsrc%5Etfw\">#php<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/ansible?src=hash&ref_src=twsrc%5Etfw\">#ansible<\/a> <a href=\"https:\/\/t.co\/5ZOPClUjvC\">https:\/\/t.co\/5ZOPClUjvC<\/a> <a href=\"https:\/\/t.co\/TWih82Ny0P\">pic.twitter.com\/TWih82Ny0P<\/a><\/p>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1151241347225071618?ref_src=twsrc%5Etfw\">July 16, 2019<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>The session that I\u2019m going to be presenting is a twenty minute version of my <a href=\"\/talks\/deploying-php-ansible-ansistrano\">Deploying PHP applications with Ansible, Ansible Vault and Ansistrano<\/a> talk.<\/p>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/speaking-drupalcon-amsterdam\/drupalcon-schedule.jpg\" alt=\"My session on the DrupalCon Amsterdam schedule.\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n My session on the DrupalCon Amsterdam schedule.\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<p>I\u2019ve been working with Drupal since 2007 (or maybe 2008), and it was the subject of my <a href=\"\/talks\/so-what-is-this-drupal-thing\">first meetup talk<\/a> in 2012. Since then I\u2019ve given <a href=\"\/talks\">48 more talks<\/a> (including one workshop) at various user groups and conferences on a range of development and systems administration topics. So it\u2019s a nice conincedence that this will be my fiftieth (50th) talk.<\/p>\n\n<p>Thanks also to my employer, <a href=\"https:\/\/inviqa.com\">Inviqa<\/a>, who are giving me the time and covering my costs to attend the conference.<\/p>\n",
|
||
"tags": ["speaking"
|
||
,"drupalcon"
|
||
,"personal"
|
||
]
|
||
}, {
|
||
"title": "Using Transition Class Props in Vue.js",
|
||
"path": "/articles/using-transition-props-vuejs",
|
||
"is_draft": "false",
|
||
"created": "1559779200",
|
||
"excerpt": "Adam Wathan shows a more Tailwind-esque approach to writing Vue.js transitions.",
|
||
"body": "<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n \ud83d\udd25 Using the transition class props instead of the <code>name<\/code> prop for <a href=\"https:\/\/twitter.com\/vuejs?ref_src=twsrc%5Etfw\">@vuejs<\/a> transitions makes it really easy to compose transitions on the fly using utility classes.<br><br>This is how I do all my transitions in Vue these days \u2014 fits a lot better with the <a href=\"https:\/\/twitter.com\/tailwindcss?ref_src=twsrc%5Etfw\">@tailwindcss<\/a> philosophy \ud83d\udc4c\ud83c\udffb <a href=\"https:\/\/t.co\/shQCxaFZ8A\">pic.twitter.com\/shQCxaFZ8A<\/a><\/p>— Adam Wathan (@adamwathan) <a href=\"https:\/\/twitter.com\/adamwathan\/status\/1118670393030537217?ref_src=twsrc%5Etfw\">April 18, 2019<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n",
|
||
"tags": ["vuejs"
|
||
]
|
||
}, {
|
||
"title": "Test Driven Ansible Role Development with Molecule",
|
||
"path": "/articles/test-driven-ansible-role-development-molecule",
|
||
"is_draft": "false",
|
||
"created": "1559433600",
|
||
"excerpt": "Some resources that I found for testing Ansible roles with a tool called Molecule.",
|
||
"body": "<p>I used to maintain a number of <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/user_guide\/playbooks_reuse_roles.html\">Ansible roles<\/a>, and I recently wrote one for automatically generating <code>settings.php<\/code> files for Drupal projects that I use for some client projects as part of the <a href=\"\/talks\/deploying-php-ansible-ansistrano\">Ansible and Ansistrano deployment process<\/a>, as it can populate these files with credentials stored in Ansible Vault.<\/p>\n\n<p>I uploaded an initial version of the role <a href=\"https:\/\/github.com\/opdavies\/ansible-role-drupal-settings\">onto GitHub<\/a>, but haven\u2019t yet released it onto Ansible Galaxy.<\/p>\n\n<p>I\u2019d seen in other people\u2019s roles and read elsewhere about writing automated tests for Ansible roles using a tool called <a href=\"https:\/\/molecule.readthedocs.io\">Molecule<\/a>, and wanted to write some tests for this role before publishing it onto Galaxy.<\/p>\n\n<p>I looked around for resources about Molecule, and found a <a href=\"https:\/\/www.jeffgeerling.com\/blog\/2018\/testing-your-ansible-roles-molecule\">blog post by Jeff Geerling<\/a>, but also this YouTube video that I found very helpful.<\/p>\n\n<p>I\u2019ve since been re-writing the role from scratch based on Molecule, and plan to release an official version of it soon.<\/p>\n\n<p><div class=\"video-full\">\n <iframe\n src=\"https:\/\/www.youtube.com\/embed\/DAnMyBZ8-Qs\"\n height=\"315\"\n width=\"560\"\n frameborder=\"0\"\n allowfullscreen\n ><\/iframe>\n<\/div>\n<\/p>\n",
|
||
"tags": ["ansible"
|
||
,"molecule"
|
||
,"testing"
|
||
,"video"
|
||
]
|
||
}, {
|
||
"title": "Speakers and sessions announced for DrupalCamp Bristol 2019",
|
||
"path": "/articles/drupalcamp-bristol-2019-speakers-sessions-announced",
|
||
"is_draft": "false",
|
||
"created": "1559260800",
|
||
"excerpt": "DrupalCamp Bristol is returning next month, and the accepted speakers and sessions have just been announced.",
|
||
"body": "<p class=\"lead\">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 <strong>Drupal in a microservice architecture<\/strong>, <strong>Automate to manage repetitive tasks with Ansible<\/strong> and <strong>Doing good with Drupal<\/strong>.<\/p>\n\n<p>Find out more about all of our sessions and speakers on <a href=\"https:\/\/2019.drupalcampbristol.co.uk\">the DrupalCamp Bristol website<\/a>, as well as view the schedule for the day.<\/p>\n\n<p>Also, at the time of writing, <a href=\"https:\/\/2019.drupalcampbristol.co.uk\/tickets\">early bird tickets are still available<\/a> for a few more days!<\/p>\n\n<p>In the meantime, the videos from the 2017 Camp are on <a href=\"https:\/\/opdavi.es\/dcbristol17-videos\">our YouTube channel<\/a>, including the opening keynote from <a href=\"https:\/\/twitter.com\/embobmaria\">Emma Karayiannis<\/a>:<\/p>\n\n<p><div class=\"video-full\">\n <iframe\n src=\"https:\/\/www.youtube.com\/embed\/honnav4YlAA\"\n height=\"315\"\n width=\"560\"\n frameborder=\"0\"\n allowfullscreen\n ><\/iframe>\n<\/div>\n<\/p>\n",
|
||
"tags": ["drupalcamp"
|
||
,"drupalcamp-bristol"
|
||
,"dcbristol"
|
||
]
|
||
}, {
|
||
"title": "Testing Tailwind CSS plugins with Jest",
|
||
"path": "/articles/testing-tailwindcss-plugins-with-jest",
|
||
"is_draft": "false",
|
||
"created": "1556496000",
|
||
"excerpt": "How to write tests for Tailwind CSS plugins using Jest.",
|
||
"body": "<div class=\"note\">\n\n<p><strong>Note:<\/strong> The content of this post is based on tests seen in Adam Wathan\u2019s <a href=\"https:\/\/www.youtube.com\/watch?v=SkTKN38wSEM\">\"Working on Tailwind 1.0\" video<\/a>, the Jest documentation website, and existing tests for other Tailwind plugins that I\u2019ve used such as <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-interaction-variants\">Tailwind CSS Interaction Variants<\/a>.<\/p>\n\n<\/div>\n\n<h2 id=\"preface\">Preface<\/h2>\n\n<p>In Tailwind 0.x, there was a <code>list-reset<\/code> utility that reset the list style and padding on a HTML list, though it was removed prior to 1.0 and moved into Tailwind\u2019s base styles and applied by default.<\/p>\n\n<p>However, on a few projects I use Tailwind in addition to either existing custom styling or another CSS framework, and don\u2019t use <code>@tailwind base<\/code> (formerly <code>@tailwind preflight<\/code>) so don\u2019t get the base styles.<\/p>\n\n<p>Whilst I could re-create this by replacing it with two other classes (<code>list-none<\/code> and <code>p-0<\/code>), I decided to write <a href=\"https:\/\/github.com\/opdavies\/tailwindcss-list-reset\">my own Tailwind CSS plugin<\/a> to re-add the <code>list-reset<\/code> class. This way I could keep backwards compatibility in my projects and only need to add one class in other future instances.<\/p>\n\n<p>In this post, I\u2019ll use this as an example to show how to write tests for Tailwind CSS plugins with a JavaScript testing framework called <a href=\"https:\/\/jestjs.io\">Jest<\/a>.<\/p>\n\n<p>More information about plugins for Tailwind CSS themselves can be found on the <a href=\"https:\/\/tailwindcss.com\/docs\/plugins\">Tailwind website<\/a>.<\/p>\n\n<h2 id=\"add-dependencies\">Add dependencies<\/h2>\n\n<p>To start, we need to include <code>jest<\/code> as a dependency of the plugin, as well as <code>jest-matcher-css<\/code> to perform assertions against the CSS that the plugin generates.<\/p>\n\n<p>We also need to add <code>tailwindcss<\/code> and <code>postcss<\/code> so that we can use them within the tests.<\/p>\n\n<pre><code class=\"plain\">yarn add -D jest jest-matcher-css postcss tailwindcss@next\n<\/code><\/pre>\n\n<p>This could be done with <code>yarn add<\/code> or <code>npm install<\/code>.<\/p>\n\n<h2 id=\"writing-the-first-test\">Writing the first test<\/h2>\n\n<p>In this plugin, the tests are going to be added into a new file called <code>test.js<\/code>. This file is automatically loaded by Jest based on it\u2019s <a href=\"https:\/\/jestjs.io\/docs\/en\/configuration#testregex-string-array-string\">testRegex setting<\/a>.<\/p>\n\n<p>This is the format for writing test methods:<\/p>\n\n<pre><code class=\"js\">test('a description of the test', () => {\n \/\/ Perform tasks and write assertions\n})\n<\/code><\/pre>\n\n<p>The first test is to ensure that the correct CSS is generated from the plugin using no options.<\/p>\n\n<p>We do this by generating the plugin\u2019s CSS, and asserting that it matches the expected CSS within the test.<\/p>\n\n<pre><code class=\"js\">test('it generates the list reset class', () => {\n generatePluginCss().then(css => {\n expect(css).toMatchCss(`\n .list-reset {\n list-style: none;\n padding: 0\n }\n `)\n })\n})\n<\/code><\/pre>\n\n<p>However, there are some additional steps needed to get this working.<\/p>\n\n<h3 id=\"generating-the-plugin%E2%80%99s-css\">Generating the plugin\u2019s CSS<\/h3>\n\n<p>Firstly, we need to import the plugin\u2019s main <code>index.js<\/code> file, as well as PostCSS and Tailwind. This is done at the beginning of the <code>test.js<\/code> file.<\/p>\n\n<pre><code class=\"js\">const plugin = require('.\/index.js')\nconst postcss = require('postcss')\nconst tailwindcss = require('tailwindcss')\n<\/code><\/pre>\n\n<p>Now we need a way to generate the CSS so assertions can be written against it.<\/p>\n\n<p>In this case, I\u2019ve created a function called <code>generatePluginCss<\/code> that accepts some optional options, processes PostCSS and Tailwind, and returns the CSS.<\/p>\n\n<pre><code class=\"js\">const generatePluginCss = (options = {}) => {\n return postcss(\n tailwindcss()\n )\n .process('@tailwind utilities;', {\n from: undefined\n })\n .then(result => result.css)\n}\n<\/code><\/pre>\n\n<p>Alternatively, to test the output of a component, <code>@tailwind utilities;<\/code> would be replaced with <code>@tailwind components<\/code>.<\/p>\n\n<pre><code class=\"js\">.process('@tailwind components;', {\n from: undefined\n})\n<\/code><\/pre>\n\n<p>Whilst <code>from: undefined<\/code> isn\u2019t required, if it\u2019s not included you will get this message:<\/p>\n\n<blockquote>\n <p>Without <code>from<\/code> option PostCSS could generate wrong source map and will not find Browserslist config. Set it to CSS file path or to <code>undefined<\/code> to prevent this warning.<\/p>\n<\/blockquote>\n\n<h3 id=\"configuring-tailwind\">Configuring Tailwind<\/h3>\n\n<p>In order for the plugin to generate CSS, it needs to be enabled within the test, and Tailwind\u2019s core plugins need to be disabled so that we can assert against just the output from the plugin.<\/p>\n\n<p>As of Tailwind 1.0.0-beta5, this can be done as follows:<\/p>\n\n<pre><code>tailwindcss({\n corePlugins: false,\n plugins: [plugin(options)]\n})\n<\/code><\/pre>\n\n<p>In prior versions, each plugin in <code>corePlugins<\/code> needed to be set to <code>false<\/code> separately.<\/p>\n\n<p>I did that using a <code>disableCorePlugins()<\/code> function and <a href=\"https:\/\/lodash.com\">lodash<\/a>, using the keys from <code>variants<\/code>:<\/p>\n\n<pre><code>const _ = require('lodash')\n\n\/\/ ...\n\nconst disableCorePlugins = () => {\n return _.mapValues(defaultConfig.variants, () => false)\n}\n<\/code><\/pre>\n\n<h3 id=\"enabling-css-matching\">Enabling CSS matching<\/h3>\n\n<p>In order to compare the generated and expected CSS, <a href=\"https:\/\/www.npmjs.com\/package\/jest-matcher-css\">the CSS matcher for Jest<\/a> needs to be required and added using <a href=\"https:\/\/jestjs.io\/docs\/en\/expect#expectextendmatchers\">expect.extend<\/a>.<\/p>\n\n<pre><code class=\"js\">const cssMatcher = require('jest-matcher-css')\n\n...\n\nexpect.extend({\n toMatchCss: cssMatcher\n})\n<\/code><\/pre>\n\n<p>Without it, you\u2019ll get an error message like <em>\"TypeError: expect(...).toMatchCss is not a function\"<\/em> when running the tests.<\/p>\n\n<h2 id=\"the-next-test%3A-testing-variants\">The next test: testing variants<\/h2>\n\n<p>To test variants we can specify the required variant names within as options to <code>generatePluginCss<\/code>.<\/p>\n\n<p>For example, this is how to enable <code>hover<\/code> and <code>focus<\/code> variants.<\/p>\n\n<pre><code class=\"js\">generatePluginCss({ variants: ['hover', 'focus'] })\n<\/code><\/pre>\n\n<p>Now we can add another test that generates the variant classes too, to ensure that also works as expected.<\/p>\n\n<pre><code class=\"js\">test('it generates the list reset class with variants', () => {\n generatePluginCss({ variants: ['hover', 'focus'] }).then(css => {\n expect(css).toMatchCss(`\n .list-reset {\n list-style: none;\n padding: 0\n }\n\n .hover\\\\:list-reset:hover {\n list-style: none;\n padding: 0\n }\n\n .focus\\\\:list-reset:focus {\n list-style: none;\n padding: 0\n }\n `)\n })\n})\n<\/code><\/pre>\n\n<h2 id=\"running-tests-locally\">Running tests locally<\/h2>\n\n<p>Now that we have tests, we need to be able to run them.<\/p>\n\n<p>With Jest included as a dependency, we can update the <code>test<\/code> script within <code>package.json<\/code> to execute it rather than returning a stub message.<\/p>\n\n<pre><code class=\"diff\">- \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n+ \"test\": \"jest\"\n<\/code><\/pre>\n\n<p>This means that as well as running the <code>jest<\/code> command directly to run the tests, we can also run <code>npm test<\/code> or <code>yarn test<\/code>.<\/p>\n\n<p>After running the tests, Jest will display a summary of the results:<\/p>\n\n<p><img src=\"\/images\/blog\/testing-tailwindcss-plugins\/running-tests.png\" alt=\"A screenshot of the Jest output after running the tests, showing 1 passed test suite and 2 passed tests, as well as the test run time.\" \/><\/p>\n\n<h2 id=\"running-tests-automatically-with-travis-ci\">Running tests automatically with Travis CI<\/h2>\n\n<p>As well as running the tests locally, they can also be run automatically via services like <a href=\"https:\/\/travis-ci.org\">Travis CI<\/a> when a new pull request is submitted or each time new commits are pushed.<\/p>\n\n<p>This is done by adding a <code>.travis-ci.yml<\/code> file to the repository, like this one which is based on the <a href=\"https:\/\/docs.travis-ci.com\/user\/languages\/javascript-with-nodejs\">JavaScript and Node.js example<\/a>:<\/p>\n\n<pre><code class=\"yml\">language: node_js\n\nnode_js:\n - '8'\n\ncache:\n directories:\n - node_modules\n\nbefore_install:\n - npm update\n\ninstall:\n - npm install\n\nscript:\n - npm test\n<\/code><\/pre>\n\n<p>With this in place, the project can now be enabled on the Travis website, and the tests will be run automatically.<\/p>\n\n<p>For this plugin, you can see the results at <a href=\"https:\/\/travis-ci.org\/opdavies\/tailwindcss-list-reset\">https:\/\/travis-ci.org\/opdavies\/tailwindcss-list-reset<\/a>.<\/p>\n",
|
||
"tags": ["javascript"
|
||
,"jest"
|
||
,"tailwind-css"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Restructuring my tailwind.js configuration files",
|
||
"path": "/articles/restructuring-my-tailwindjs-config-files",
|
||
"is_draft": "false",
|
||
"created": "1552003200",
|
||
"excerpt": "How I\u2019ve started structuring my tailwind.js configuration files in preparation for Tailwind 1.0.",
|
||
"body": "<p>After watching Adam Wathan\u2019s recent <a href=\"https:\/\/www.youtube.com\/watch?v=SkTKN38wSEM\">\"Working on Tailwind 1.0\" YouTube video<\/a> and seeing some of the proposed changes to the <code>tailwind.js<\/code> configuration file, I\u2019ve started to structure my current config files a little differently in preparation for 1.0.<\/p>\n\n<h2 id=\"the-current-tailwind.js-file-format\">The current tailwind.js file format<\/h2>\n\n<p>Currently when you run <code>tailwind init<\/code> to create a new config file, it includes all of Tailwind\u2019s default values, and then you can add, edit and remove values as needed.<\/p>\n\n<p>Some values like colours, font families, plugins and modules you are likely to change for each project, whilst others like shadows, leading, z-index and opacity, you\u2019re less likely to need to change.<\/p>\n\n<p>It\u2019s 952 lines including comments, which is quite long and could potentially be daunting for new Tailwind users.<\/p>\n\n<p>The contents of the full file can be found in the <a href=\"https:\/\/tailwindcss.com\/docs\/configuration#default-configuration\">Tailwind CSS documentation<\/a>, or it can be found in <a href=\"https:\/\/github.com\/tailwindcss\/plugin-examples\/blob\/master\/tailwind.js\">various GitHub repositories<\/a>.<\/p>\n\n<h2 id=\"a-preview-of-the-new-tailwind.js-file-format\">A preview of the new tailwind.js file format<\/h2>\n\n<p>In Adam\u2019s <a href=\"https:\/\/laracon.net\">Laracon Online<\/a> talk, Tailwind CSS by Example, he showed the new configuration file format. Here is a snippet:<\/p>\n\n<pre><code class=\"js\">module.exports {\n theme: {\n extend: {\n spacing: {\n 7: '1.75rem',\n },\n },\n colors: {\n white: {\n default: '#fff',\n 20: 'rgba(255,255,255,.2)',\n 40: 'rgba(255,255,255,.4)',\n 60: 'rgba(255,255,255,.6)',\n 80: 'rgba(255,255,255,.8)',\n },\n ...\n }\n ...\n }\n}\n<\/code><\/pre>\n\n<p>You\u2019ll notice that the structure of the file is quite different, and that all of the default values have been removed and are now maintained by Tailwind itself.<\/p>\n\n<p>This means that the configuration file contains only your custom changes, where you've either overridden a default value (e.g. colours) or added your own using <code>extend<\/code> (e.g. adding another spacing value, as in this example).<\/p>\n\n<p>I think that's a great improvement and makes the file so much cleaner, and easier to read and understand.<\/p>\n\n<h2 id=\"an-interim-approach\">An interim approach<\/h2>\n\n<p>If you don\u2019t want to wait until 1.0, or potentially 0.8, you can get some of this functionality now by restructuring your Tailwind configuration file.<\/p>\n\n<p>Here is the complete <code>tailwind.js<\/code> file for the <a href=\"https:\/\/dcb-2019-static.netlify.com\">DrupalCamp Bristol 2019 static landing page<\/a>, which uses Tailwind in addition to the existing traditional CSS:<\/p>\n\n<pre><code class=\"js\">let defaultConfig = require('tailwindcss\/defaultConfig')()\n\nvar colors = {\n ...defaultConfig.colors,\n black: '#000',\n}\n\nmodule.exports = {\n ...defaultConfig,\n colors: colors,\n textColors: colors,\n backgroundColors: colors,\n borderColors: Object.assign({ default: colors['grey-light'] }, colors),\n plugins: [\n require('tailwindcss-interaction-variants')(),\n require('tailwindcss-spaced-items'),\n ],\n modules: {\n ...defaultConfig.modules,\n textStyle: [...defaultConfig.modules.textStyle, 'hocus'],\n },\n options: {\n ...defaultConfig.options,\n prefix: 'tw-',\n important: true,\n },\n}\n<\/code><\/pre>\n\n<p>Here are the steps that I took to create this file:<\/p>\n\n<ol class=\"spaced-y-6\">\n <li>\n <p><strong>Get the default configuration<\/strong>. This is done using <code>require('tailwindcss\/defaultConfig')()<\/code>. Essentially this has the same contents as the current <code>tailwind.js<\/code> file, though now it\u2019s owned and maintained within Tailwind itself, and not by the user.<\/p>\n <p>Also any new or updated values within the default configuration will be automatically available.<\/p>\n <p>This line is present but commented out in the current generated <code>tailwind.js<\/code> file.<\/p>\n <\/li>\n\n <li>\n <p><strong>Create the colors object.<\/strong> This will by default override Tailwind\u2019s default colours, however you can add <code>...defaultConfig.colors<\/code> to include them and then add or edit values as needed.<\/p>\n <p>In this example, I\u2019m overridding the value used for the <code>black<\/code> colour classes to match the existing colour in the other CSS.<\/p>\n <\/li>\n\n <li>\n <p><strong>Return the main configuration object.<\/strong> For sites with no overrides, this could just be <code>module.exports = defaultConfig<\/code> for a minimal configuration.<\/p>\n <p>To extend the defaults, add <code>...defaultConfig<\/code> at the beginning.<\/p>\n <\/li>\n\n <li>\n <p><strong>Assign our colours.<\/strong> Use them for <code>colors<\/code>, <code>textColors<\/code>, <code>backgroundColors<\/code> and <code>borderColours<\/code>.<\/p>\n <\/li>\n\n <li>\n <p><strong>Add any plugins<\/strong>. I use plugins on most projects, in this case I\u2019m using <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-interaction-variants\">tailwindcss-interaction-variants<\/a> and <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-spaced-items\">tailwindcss-spaced-items<\/a>. Usually the default <code>container<\/code> plugin would be here too.<\/p>\n <\/li>\n\n <li>\n <p><strong>Add or override modules.<\/strong> Here I\u2019m adding the <code>hocus<\/code> (hover and focus) variant provided by the interaction variants plugin to the text style classes.<\/p>\n <\/li>\n\n <li>\n <p><strong>Add or override options.<\/strong> As this markup was originally from a Drupal website, I needed to override some of the options values. I\u2019ve added the <code>tw-<\/code> prefix to avoid Tailwind classes from clashing with Drupal\u2019s default markup, and set all Tailwind classes to use <code>!important<\/code> so that they override any existing styles.<\/p>\n <\/li>\n<\/ol>\n\n<p>This file is only 27 lines long, so considerably shorter than the default file, and I think that it\u2019s much easier to see what your additional and overridden values are, as well able to quickly recognise whether a class is generated from a custom value or from a Tailwind default value.<\/p>\n\n<p>To move this file to the new format I think would be much easier as there\u2019s no default configuration to filter out, and you can move across only what is needed.<\/p>\n\n<h2 id=\"other-changes\">Other changes<\/h2>\n\n<h3 id=\"consistent-spacing-for-padding-and-margin\">Consistent spacing for padding and margin<\/h3>\n\n<p>Similar to defining colours, you could also set some standard spacing values, and using those for padding, margin and negative margin to ensure that they are all consistent.<\/p>\n\n<p>In this case, we can use <code>defaultConfig.margin<\/code> to get the default, add or override any values, and then assign it to the relevant sections of the main object.<\/p>\n\n<pre><code class=\"js\">const spacing = {\n ...defaultConfig.margin,\n '2px': '2px',\n}\n\nmodule.exports = {\n ...defaultConfig,\n \/\/ ...\n padding: spacing,\n margin: spacing,\n negativeMargin: spacing,\n \/\/ ...\n}\n<\/code><\/pre>\n\n<h3 id=\"picking-values-with-lodash\">Picking values with lodash<\/h3>\n\n<p>In the opposite to extending, if we wanted to limit the number of values within a part of the configuration, we can do that too. I\u2019d suggest using the <a href=\"https:\/\/lodash.com\/docs\/4.17.11#pick\">pick method<\/a> provided by <a href=\"https:\/\/lodash.com\">Lodash<\/a>.<\/p>\n\n<p>From the documentation:<\/p>\n\n<blockquote>\n <p>Creates an object composed of the picked object properties.<\/p>\n<\/blockquote>\n\n<p>For example, if we only wanted normal, medium and bold font weights:<\/p>\n\n<pre><code class=\"js\">module.exports = {\n ...defaultConfig,\n \/\/ ...\n fontWeights: _.pick(defaultConfig.fontWeights, ['normal', 'medium', 'bold']),\n \/\/ ...\n}\n<\/code><\/pre>\n\n<h3 id=\"renaming-the-file\">Renaming the file<\/h3>\n\n<p>Also in Tailwind 1.0, it seems that the configuration file name is changing from <code>tailwind.js<\/code> to <code>tailwind.config.js<\/code>.<\/p>\n\n<p>If you use <a href=\"https:\/\/laravel-mix.com\">Laravel Mix<\/a> and the <a href=\"https:\/\/github.com\/JeffreyWay\/laravel-mix-tailwind\">Laravel Mix Tailwind plugin<\/a> like I do on this site (even though it\u2019s a Sculpin site), it will look for a <code>tailwind.js<\/code> file by default or you can specify whatever filename you need.<\/p>\n\n<p>Here is an excerpt of the Tailwind configuration file for this site, using <code>tailwind.config.js<\/code>:<\/p>\n\n<pre><code class=\"js\">mix.postCss('assets\/css\/app.css', 'source\/dist\/css')\n .tailwind('tailwind.config.js')\n<\/code><\/pre>\n\n<h2 id=\"looking-foward-to-tailwind-css-1.0%21\">Looking foward to Tailwind CSS 1.0!<\/h2>\n\n<p>Adam has said that Tailwind 1.0 should be released within a few weeks of the time of writing this, I assume once <a href=\"https:\/\/github.com\/tailwindcss\/tailwindcss\/issues\/692\">the 1.0 To-Do list<\/a> is completed.<\/p>\n\n<p>I really like some of the improvements that are coming in 1.0, including the new configuration file format and the ability to easily add and extend values, as well as the file itself now being completely optional.<\/p>\n\n<p>I can\u2019t wait for it to land!<\/p>\n",
|
||
"tags": ["laravel-mix"
|
||
,"tailwind-css"
|
||
]
|
||
}, {
|
||
"title": "Easier Git Repository Cloning with insteadOf",
|
||
"path": "/articles/easier-git-repository-cloning-with-insteadof",
|
||
"is_draft": "false",
|
||
"created": "1551916800",
|
||
"excerpt": "How to simplify 'git clone' commands by using the insteadOf configuration option within your .gitconfig file.",
|
||
"body": "<p>When working on client or open source projects, I clone a lot of <a href=\"https:\/\/git-scm.com\">Git<\/a> repositories - either from GitHub, GitLab, Bitbucket or Drupal.org.\nThe standard <code>git clone<\/code> commands though provided by these sites can be quite verbose with long repository URLs and use a mixture of different protocols, and I\u2019d regularly need to go back to each website and look up the necessary command every time.<\/p>\n\n<p>For example, here is the command provided to clone Drupal\u2019s <a href=\"https:\/\/www.drupal.org\/project\/override_node_options\">Override Node Options module<\/a>:<\/p>\n\n<pre><code class=\"plain\">git clone --branch 8.x-2.x https:\/\/git.drupal.org\/project\/override_node_options.git\n<\/code><\/pre>\n\n<p>We can though simplify the command to make it easier and quicker to type, using a Git configuration option called <code>insteadOf<\/code>.<\/p>\n\n<h2 id=\"what-is-insteadof%3F\">What is insteadOf?<\/h2>\n\n<p>From the <a href=\"https:\/\/git-scm.com\/docs\/git-config#git-config-urlltbasegtinsteadOf\">Git documentation<\/a>:<\/p>\n\n<blockquote>\n <p><strong>url.[base].insteadOf:<\/strong><\/p>\n \n <p>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.<\/p>\n<\/blockquote>\n\n<p>Whilst examples are sparse, <a href=\"https:\/\/stackoverflow.com\/questions\/1722807\/how-to-convert-git-urls-to-http-urls\">it seems like<\/a> insteadOf is used for resolving protocol issues with repository URLs. However, we can use it to simplify our clone commands, as mentioned above.<\/p>\n\n<h2 id=\"example%3A-cloning-drupal-contrib-projects\">Example: cloning Drupal contrib projects<\/h2>\n\n<p>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.<\/p>\n\n<p>Again, here is the provided command to clone the Override Node Options module:<\/p>\n\n<pre><code class=\"plain\">git clone --branch 8.x-2.x https:\/\/git.drupal.org\/project\/override_node_options.git\n<\/code><\/pre>\n\n<p>At the time of writing, the Git repository URL follow this same format - <code>https:\/\/git.drupal.org\/project\/{name}.git<\/code> (also the <code>.git<\/code> file extension is optional).<\/p>\n\n<p>To shorten and simplify this, I can add this snippet to my <code>~\/.gitconfig<\/code> file:<\/p>\n\n<pre><code>[url \"https:\/\/git.drupal.org\/project\/\"]\n insteadOf = do:\n insteadOf = drupal:\n<\/code><\/pre>\n\n<p>With that added, I can now instead run <code>git clone drupal:{name}<\/code> or <code>git clone do:{name}<\/code> to clone the repository, specifying the project\u2019s machine name.<\/p>\n\n<p>For example, to clone the Override Node Options module, I can now do this using just <code>git clone drupal:override_node_options<\/code>.<\/p>\n\n<p>This, I think, is definitely quicker and easier!<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<p>You can view my entire <code>.gitconfig<\/code> file, as well as my other dotfiles, in <a href=\"https:\/\/github.com\/opdavies\/dotfiles\/blob\/master\/.gitconfig\">my dotfiles repository on GitHub<\/a>.<\/p>\n",
|
||
"tags": ["git"
|
||
]
|
||
}, {
|
||
"title": "Rebuilding Bartik (Drupal’s Default Theme) with Vue.js and Tailwind CSS - part 2",
|
||
"path": "/articles/rebuilding-bartik-with-vuejs-tailwind-css-part-2",
|
||
"is_draft": "false",
|
||
"created": "1545868800",
|
||
"excerpt": "A follow-up to my original post on rebuilding Bartik with Tailwind and Vue.js.",
|
||
"body": "<p>In <a href=\"\/blog\/rebuilding-bartik-with-vuejs-tailwind-css\">the original post<\/a> I detailed how I built <a href=\"https:\/\/rebuilding-bartik.oliverdavies.uk\">a clone of Drupal\u2019s Bartik theme<\/a> with <a href=\"https:\/\/vuejs.org\">Vue.js<\/a> and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a>. This follow-up post details some updates that I\u2019ve made to it since then.<\/p>\n\n<h2 id=\"customising-tailwind%E2%80%99s-colours\">Customising Tailwind\u2019s colours<\/h2>\n\n<p>During the first version of the page, my thoughts were to not edit the Tailwind configuration, however I changed my mind on this whilst working on the subsequent updates and did make some changes and customisations to the <code>tailwind.js<\/code> file.<\/p>\n\n<p>By default, Tailwind includes a full colour palette including colours such as yellows, oranges, reds that weren\u2019t being used in this page so they were removed. This makes the file more readable as well as reduces the number of classes that Tailwind generates.<\/p>\n\n<p>Whist I was changing the colours, I also took the opportunity to tweak the values of the remaining colours to more closely match Bartik\u2019s original colours.<\/p>\n\n<p>I also added a <code>black-60<\/code> class which uses <a href=\"https:\/\/css-tricks.com\/the-power-of-rgba\">RGBA<\/a> to provide a semi-transparent background. I used this when adding the <a href=\"#adding-the-skip-to-main-content-link\">skip to main content link<\/a>.<\/p>\n\n<pre><code class=\"js\">let colors = {\n 'transparent': 'transparent',\n\n 'black': '#22292f',\n 'grey-darkest': '#3d4852',\n 'grey-dark': '#8795a1',\n 'grey': '#b8c2cc',\n 'grey-light': '#dae1e7',\n 'grey-lighter': '#f0f0f0',\n 'grey-lightest': '#F6F6F2',\n 'white': '#ffffff',\n\n 'black-60': 'rgba(0, 0, 0, .6)',\n\n 'blue-dark': '#2779bd',\n 'blue': '#3490dc',\n 'blue-light': '#bcdefa',\n\n 'green-dark': '#325E1C',\n 'green': '#77B159',\n 'green-light': '#CDE2C2',\n 'green-lighter': '#F3FAEE',\n}\n<\/code><\/pre>\n\n<h2 id=\"adding-default-styling-for-links\">Adding default styling for links<\/h2>\n\n<p>In the first version, every link was individually styled which resulted in a lot of duplicate classes and a potential maintenance issue.<\/p>\n\n<p>I added a <code>style<\/code> section within <code>Welcome.vue<\/code>, and added some default styling for links based on their location on the page - <a href=\"https:\/\/tailwindcss.com\/docs\/extracting-components\">extracting some Tailwind components<\/a>.<\/p>\n\n<div v-pre>\n\n<p><\/p>\n\n<pre><code class=\"vuejs\"><template>\n ...\n\n <div id=\"footer\" class=\"text-xs text-white\">\n <div class=\"container mx-auto px-4 pt-16 pb-4\">\n <div class=\"border-t border-solid border-gray-900 pt-6 -mb-6\">\n <div class=\"mb-6\">\n <p><a href=\"#0\">Contact<\/a><\/p>\n <\/div>\n\n <div class=\"mb-6\">\n <p>\n A clone of <a href=\"https:\/\/www.drupal.org\">Drupal<\/a>\u2019s default theme (Bartik).\n Built by <a href=\"https:\/\/www.oliverdavies.uk\">Oliver Davies<\/a>\n using <a href=\"https:\/\/vuejs.org\">Vue.js<\/a>\n and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a>.\n <\/p>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/template>\n<\/code><\/pre>\n\n<p>Within the <code>style<\/code> section, I\u2019m able to use Tailwind\u2019s custom <code>@apply<\/code> directive to inject it\u2019s rules into more traditional CSS, rather than needing to add them onto every link.<\/p>\n\n<pre><code class=\"vuejs\"><style lang=\"sass\">\n#header a\n @apply text-white no-underline\n\n &:hover,\n &:focus\n @apply underline\n\n#main a\n @apply text-blue-dark no-underline border-b border-blue-600 border-dotted\n\n &:hover,\n &:focus\n @apply text-blue-600 border-solid\n\n#footer a\n @apply text-white no-underline border-b border-dotted border-white\n\n &:hover,\n &:focus\n @apply border-none\n<\/style>\n<\/code><\/pre>\n\n<p><\/p>\n\n<\/div>\n\n<h2 id=\"extracting-a-vue-component-for-drupal-blocks\">Extracting a Vue component for Drupal blocks<\/h2>\n\n<p>As well as being able to extract re-usable components within Tailwind, the same can be done within Vue. As the page could potentially have multiple sidebar blocks, I extracted a <code>SidebarBlock<\/code> component which would act as a wrapper around the block\u2019s contents.<\/p>\n\n<pre><code class=\"vuejs\">\/\/ src\/components\/Sidebar.vue\n\n<template>\n <div class=\"bg-gray-200 p-4 mb-4\">\n <slot><\/slot>\n <\/div>\n<\/template>\n<\/code><\/pre>\n\n<p>The component provides the wrapping div and the appropriate classes in a single easy-to-maintain location, and <a href=\"https:\/\/vuejs.org\/v2\/guide\/components-slots.html\">uses a slot<\/a> as a placeholder for the main content.<\/p>\n\n<p>That means that within <code>Welcome.vue<\/code>, the markup within the <code>sidebar-block<\/code> tags will be used as the block contents.<\/p>\n\n<pre><code class=\"html\"><sidebar-block>\n <p>My block contents.<\/p>\n<\/sidebar-block>\n<\/code><\/pre>\n\n<h2 id=\"adding-the-skip-to-main-content-link\">Adding the Skip to Main Content Link<\/h2>\n\n<p>One thing <a href=\"https:\/\/github.com\/opdavies\/rebuilding-bartik\/issues\/1\">that was missing<\/a> was the 'Skip to main content link'. This an accessibility feature that allows for users who are navigating the page using only a keyboard to bypass the navigation links and skip straight to the main content if they wish by clicking a link that is hidden and only visible whilst it\u2019s focussed on.<\/p>\n\n<p>Here is the markup that I used, which is placed directly after the opening <code><body><\/code> tag.<\/p>\n\n<pre><code class=\"html\"><a href=\"#0\" class=\"skip-link text-white bg-black-60 py-1 px-2 rounded-b-lg focus:no-underline focus:outline-none\">\n Skip to main content\n<\/a>\n<\/code><\/pre>\n\n<p>I initially tried to implement the same feature on this website using <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-visuallyhidden\">Tailwind\u2019s visually hidden plugin<\/a> which also contains a <code>focussable<\/code> class, though I wasn\u2019t able to style it the way that I needed. I created my own <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-skip-link\">Tailwind skip link plugin<\/a> and moved the re-usable styling there.<\/p>\n\n<p>To enable the plugin, I needed to add it within the <code>plugins<\/code> section of my <code>tailwind.js<\/code> file:<\/p>\n\n<pre><code class=\"js\">plugins: [\n require('tailwindcss\/plugins\/container')(),\n require('tailwindcss-skip-link')(),\n],\n<\/code><\/pre>\n\n<p>I added only the page-specific styling classes to the link (as well as the <code>skip-link<\/code> class that the plugin requires) as well as my own focus state to the skip link that I did within the <code>style<\/code> section of <code>App.vue<\/code>.<\/p>\n\n<pre><code class=\"vuejs\"><style lang=\"sass\">\n@tailwind preflight\n@tailwind components\n\n.skip-link:focus\n left: 50%\n transform: translateX(-50%)\n\n@tailwind utilities\n<\/style>\n<\/code><\/pre>\n\n<p><img src=\"\/images\/blog\/rebuilding-bartik-vue-tailwind-part-2\/skip-link.png\" alt=\"The Bartik clone with the skip to main content link visible\" title=\"\" class=\"border\" \/><\/p>\n\n<h2 id=\"adding-the-drupalmessage-component\">Adding the DrupalMessage component<\/h2>\n\n<p>I also added a version of Drupal\u2019s status message as another Vue component. This also uses a slot to include the message contents and accepts a <a href=\"https:\/\/vuejs.org\/v2\/guide\/components-props.html\">prop<\/a> for the message type.<\/p>\n\n<pre><code class=\"html\"><template>\n <div :class=\"[ wrapperClasses, wrapperClasses ? 'pl-2 rounded-sm' : '' ]\">\n <div class=\"py-4 pl-3 pr-4 mb-4 border flex items-center rounded-sm\" :class=\"classes\">\n <svg v-if=\"type == 'status'\" class=\"fill-current w-4 h-4 text-green mr-3\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><path d=\"M6.464 13.676a.502.502 0 0 1-.707 0L.797 8.721a.502.502 0 0 1 0-.707l1.405-1.407a.5.5 0 0 1 .707 0l2.849 2.848a.504.504 0 0 0 .707 0l6.629-6.626a.502.502 0 0 1 .707 0l1.404 1.404a.504.504 0 0 1 0 .707l-8.741 8.736z\"\/><\/svg>\n <slot><\/slot>\n <\/div>\n <\/div>\n<\/template>\n<\/code><\/pre>\n\n<p>The value of the <code>type<\/code> prop is then used within some computed properties to determine the type specific classes to add (e.g. green for success, and red for warning), as well as whether or not to include the checkmark SVG image.<\/p>\n\n<pre><code class=\"js\"><script>\nexport default {\n props: {\n type: String,\n },\n\n computed: {\n classes: function () {\n return {\n status: 'border-green-light text-green-dark bg-green-lighter',\n }[this.type]\n },\n\n wrapperClasses: function () {\n return {\n status: 'bg-green',\n }[this.type]\n },\n }\n}\n<\/script>\n<\/code><\/pre>\n\n<p>I did need to make one change to the <code>tailwind.js<\/code> file in order to change the border on links when they are hovered over - within <code>modules<\/code> I needed to enable the <code>borderStyle<\/code> module for hover and focus states in order for Tailwind to generate the additional classes.<\/p>\n\n<pre><code class=\"js\">modules: {\n \/\/ ...\n borderStyle: ['responsive', 'hover', 'focus'],\n \/\/ ...\n}\n<\/code><\/pre>\n\n<p>The message is included within the Welcome component by including the <code><drupal-message \/><\/code> element, though rather than importing it there, it\u2019s registed as a global component so it would be available to any other components that could be added in the future.<\/p>\n\n<p>This is done within <code>main.js<\/code>:<\/p>\n\n<pre><code class=\"js\">\/\/ ...\n\nVue.component('drupal-message', require('@\/components\/DrupalMessage').default)\n\nnew Vue({\n render: h => h(App),\n}).$mount('#app')\n<\/code><\/pre>\n\n<p><img src=\"\/images\/blog\/rebuilding-bartik-vue-tailwind-part-2\/drupal-message.png\" alt=\"The Bartik clone with the Drupal Message component visible\" title=\"\" class=\"border\" \/><\/p>\n\n<p><strong>The updated version is <a href=\"https:\/\/rebuilding-bartik.oliverdavies.uk\">live on Netlify<\/a>, and the <a href=\"https:\/\/github.com\/opdavies\/rebuilding-bartik\">latest source code is available on GitHub<\/a>.<\/strong><\/p>\n",
|
||
"tags": ["drupal"
|
||
,"tailwind-css"
|
||
,"tweet"
|
||
,"vuejs"
|
||
]
|
||
}, {
|
||
"title": "Published my first NPM package",
|
||
"path": "/articles/published-my-first-npm-package",
|
||
"is_draft": "false",
|
||
"created": "1544918400",
|
||
"excerpt": "Yesterday I published my first module onto NPM, and it\u2019s a plugin for Tailwind CSS to be used alongside Vue.js.",
|
||
"body": "<p>Yesterday I published my first module onto NPM, and it\u2019s a plugin for <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a> to be used alongside <a href=\"https:\/\/vuejs.org\">Vue.js<\/a>.<\/p>\n\n<p>The plugin adds classes for showing and hiding elements in different display variations in combination with Vue's <a href=\"https:\/\/vuejs.org\/v2\/api\/#v-cloak\">v-cloak directive<\/a>, which I originally saw in <a href=\"https:\/\/youtu.be\/XUXpcbYQ_iQ?t=2360\">the first 'Building Kitetail' video<\/a>. These are useful for when you want an element to be visible whilst Vue is compiling, and hidden afterwards.<\/p>\n\n<p>Here is the compiled CSS that is added by the plugin:<\/p>\n\n<pre><code class=\"css\">[v-cloak] .v-cloak-block {\n display: block;\n}\n\n[v-cloak] .v-cloak-flex {\n display: flex;\n}\n\n[v-cloak] .v-cloak-hidden {\n display: none;\n}\n\n[v-cloak] .v-cloak-inline {\n display: inline;\n}\n\n[v-cloak] .v-cloak-inline-block {\n display: inline-block;\n}\n\n[v-cloak] .v-cloak-inline-flex {\n display: inline-flex;\n}\n\n[v-cloak] .v-cloak-invisible {\n visibility: hidden;\n}\n\n.v-cloak-block,\n.v-cloak-flex,\n.v-cloak-inline,\n.v-cloak-inline-block,\n.v-cloak-inline-flex {\n display: none;\n}\n<\/code><\/pre>\n\n<p>The <code>v-cloak<\/code> directive exists on an element until Vue finishes compiling, after which it is removed. Therefore adding a <code>v-cloak-block<\/code> class to an element will make it <code>display: block<\/code> whilst Vue is compiling and the element is cloaked, and <code>display: none<\/code> afterwards when the Vue markup is compiled and rendered.<\/p>\n\n<p>In my <code>base.html.twig<\/code> template, I\u2019ve added <code>v-cloak<\/code> to the wrapper div within the <code>body<\/code>.<\/p>\n\n<p><\/p>\n\n<div v-pre>\n\n<pre><code class=\"twig\"><body class=\"font-sans leading-normal\">\n <div id=\"app\" v-cloak>\n {# ... #}\n <\/div>\n<\/body>\n<\/code><\/pre>\n\n<\/div>\n\n<p><\/p>\n\n<p>Within my <code>navbar.html.twig<\/code> partial, I have a placeholder div that also contains the site name, which is instantly visible but has the <code>v-cloak-block<\/code> class so it\u2019s hidden once Vue has compiled and the <code>Navbar<\/code> Vue component is visible instead.<\/p>\n\n<p><\/p>\n\n<div v-pre>\n\n<pre><code class=\"twig\"><div class=\"border-bottom border-b border-gray-300 mb-6\">\n <div class=\"container mx-auto\">\n <div class=\"block py-5 v-cloak-block\">\n {{ site.title }}\n <\/div>\n\n <navbar\n site-name=\"{{ site.title }}\"\n page-url=\"{{ page.url }}\"\n ><\/navbar>\n <\/div>\n<\/div>\n<\/code><\/pre>\n\n<\/div>\n\n<p><\/p>\n\n<p>I was originally surprised that these classes weren\u2019t included as part of Tailwind or as part of an existing plugin, but as I\u2019ve 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.<\/p>\n\n<p><strong>You can view <a href=\"https:\/\/www.npmjs.com\/package\/tailwindcss-vuejs\">the package on npmjs.com<\/a>, and <a href=\"https:\/\/github.com\/opdavies\/tailwindcss-vuejs\">the code repository on GitHub<\/a>.<\/strong><\/p>\n",
|
||
"tags": ["npm"
|
||
,"tailwind-css"
|
||
,"vuejs"
|
||
]
|
||
}, {
|
||
"title": "DrupalCamp London 2019 - Tickets Available and Call for Sessions",
|
||
"path": "/articles/drupalcamp-london-2019-tickets",
|
||
"is_draft": "false",
|
||
"created": "1542754800",
|
||
"excerpt": "DrupalCamp London early-bird tickets are now available, and their call for sessions is open.",
|
||
"body": "<p>It was announced this week that <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/1064584179113971712\">early-bird tickets are now available<\/a> for <a href=\"https:\/\/drupalcamp.london\">DrupalCamp London 2019<\/a>, as well as their <a href=\"https:\/\/drupalcamp.london\/get-involved\/submit-a-session\">call for sessions being open<\/a>.<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<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>\n\n<p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/1064584179113971712?ref_src=twsrc%5Etfw\">November 19, 2018<\/a>\n\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>I\u2019ve attended, given talks and volunteered previously, would definitely recommend others doing so, and I plan on attending and submitting again myself for 2019.\nIf there\u2019s something in particular that you\u2019d like to see me give a talk on, let me know - I\u2019d be happy to hear any suggestions.\nAlternatively, if you\u2019d like to submit and would like some help writing an abstract or want some feedback on a talk idea, please get in touch.<\/p>\n\n<p><em>Note: I am not an organiser of DrupalCamp London, nor am I involved with the session selection process.<\/em><\/p>\n\n<p>Hopefully there will be no <a href=\"\/articles\/tweets-drupalcamp-london\">#uksnow<\/a> this year!<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["conferences"
|
||
,"drupal"
|
||
,"drupalcamp"
|
||
,"drupalcamp-london"
|
||
]
|
||
}, {
|
||
"title": "Rebuilding Bartik (Drupal’s Default Theme) with Vue.js and Tailwind CSS",
|
||
"path": "/articles/rebuilding-bartik-with-vuejs-tailwind-css",
|
||
"is_draft": "false",
|
||
"created": "1542672000",
|
||
"excerpt": "How I rebuilt Drupal\u2019s Bartik theme using Vue.js and Tailwind CSS.",
|
||
"body": "<p>Earlier this week, I built a clone of <a href=\"https:\/\/www.drupal.org\">Drupal<\/a>\u2019s default theme, Bartik, with <a href=\"https:\/\/vuejs.org\">Vue.js<\/a> and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a>. You can <a href=\"https:\/\/github.com\/opdavies\/rebuilding-bartik\">view the code on GitHub<\/a> and the <a href=\"https:\/\/rebuilding-bartik.oliverdavies.uk\">site itself on Netlify<\/a>.<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <\/p>\n\n<p lang=\"en\" dir=\"ltr\">I built a clone of Bartik, <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a>'s default theme, with <a href=\"https:\/\/twitter.com\/vuejs?ref_src=twsrc%5Etfw\">@vuejs<\/a> and <a href=\"https:\/\/twitter.com\/tailwindcss?ref_src=twsrc%5Etfw\">@tailwindcss<\/a>. See the result at <a href=\"https:\/\/t.co\/nPsTt2cawL\">https:\/\/t.co\/nPsTt2cawL<\/a>, and the code at <a href=\"https:\/\/t.co\/Dn8eysV4gf\">https:\/\/t.co\/Dn8eysV4gf<\/a>.<br><br>Blog post coming soon... <a href=\"https:\/\/t.co\/7BgqjmkCX0\">pic.twitter.com\/7BgqjmkCX0<\/a><\/p>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1064906717392191488?ref_src=twsrc%5Etfw\">November 20, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<h2 id=\"why-build-a-bartik-clone%3F\">Why build a Bartik clone?<\/h2>\n\n<p>I\u2019m a big fan of utility based styling and Tailwind CSS in particular, I was and originally thinking of a way to more easily integrate Tailwind within Drupal - something like I\u2019ve since done with the <a href=\"https:\/\/www.drupal.org\/project\/tailwindcss\">Tailwind CSS starter kit theme<\/a>. Whilst thinking about that, I wondered about doing the opposite - rebuilding Drupal (or Bartik) with Tailwind.<\/p>\n\n<p>Others including <a href=\"https:\/\/adamwathan.me\">Adam Wathan<\/a> (one of the creators of Tailwind CSS) have rebuilt existing UIs like Netlify, YouTube, Twitter, Coinbase and Transistor.fm with Tailwind as an opportunity for learning and also to demonstrate using Tailwind - this was my opportunity to do the same.<\/p>\n\n<p>Whilst <a href=\"https:\/\/dri.es\/drupal-looking-to-adopt-react\">Drupal itself has adoped React<\/a>, I\u2019ve personally been looking into Vue.js and have used it for some small personal projects, including some elements of the site. So I decided to use Vue for the interactive parts of my Bartik clone to create a fully functional clone rather than focussing only on the CSS.<\/p>\n\n<h2 id=\"building-a-static-template-with-tailwind\">Building a static template with Tailwind<\/h2>\n\n<p>The first stage was to build the desktop version, which was done as a simple HTML file with Tailwind CSS pulled in from it\u2019s CDN. This stage took just over an hour to complete.<\/p>\n\n<p>As Tailwind was added via a CDN, there was no opportunity to customise it\u2019s configuration, so I needed to use to Tailwind\u2019s default configuration for colours, padding, spacing, fonts, breakpoints etc. The page is built entirely with classes provided by Tailwind and uses no custom CSS, except for one inline style that is used to add the background colour for the Search block, as there wasn\u2019t a suitable Tailwind option.<\/p>\n\n<p>When I decided that I was going to later add some interactivity onto the mobile navigation menu, the existing code was ported into a new Vue.js application generated by the Vue CLI, with the majority of the markup within a <code>Welcome<\/code> component. This meant that Tailwind was also added as a dependency with it\u2019s own configuration file, though although I had the opportunity to customise it I decided not to and made no changes to it and continued with the default values.<\/p>\n\n<p><code>src\/App.vue<\/code>:<\/p>\n\n<pre><code><template>\n <div id=\"app\">\n <Welcome title=\"Rebuilding Bartik\"\/>\n <\/div>\n<\/template>\n\n<script>\nimport Welcome from '.\/components\/Welcome.vue'\n\nexport default {\n name: 'app',\n components: {\n Welcome,\n }\n}\n<\/script>\n\n<style lang=\"postcss\">\n@tailwind preflight;\n@tailwind components;\n@tailwind utilities;\n<\/style>\n<\/code><\/pre>\n\n<p><code>src\/components\/Welcome.vue<\/code>:<\/p>\n\n<div v-pre>\n\n<p><\/p>\n\n<pre><code class=\"vuejs\"><template>\n <div>\n <div class=\"bg-blue-dark\">\n <div class=\"py-4 text-white\">\n <div id=\"header\" class=\"container mx-auto px-4 relative\">\n <div class=\"flex flex-col-reverse\">\n <div class=\"flex items-center\">\n <img src=\"img\/logo.svg\" alt=\"\" class=\"mr-4\">\n <div class=\"text-2xl\">\n <a href=\"#0\">{{ title }}<\/a>\n <\/div>\n <\/div>\n\n <div class=\"text-sm flex justify-end\">\n <a href=\"#0\">Log in<\/a>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n\n <main-menu><\/main-menu>\n <\/div>\n\n <div class=\"bg-white pt-3 pb-4 lg:pb-12\">\n <div class=\"container mx-auto px-4\">\n <div class=\"flex flex-col md:flex-row-reverse md:-mx-8 my-6\">\n <div id=\"main\" class=\"w-full md:w-auto md:flex-1 md:px-6 mb-8 md:mb-0\">\n <div class=\"font-serif\">\n <h1 class=\"font-normal\">Welcome to {{ title }}<\/h1>\n <p>No front page content has been created yet.<\/p>\n <p>Follow the <a href=\"#0\" class=\"text-blue-dark hover:text-blue-600 no-underline border-b border-blue-600 border-dotted hover:bg-solid\">User Guide<\/a> to start building your site.<\/p>\n <\/div>\n\n <div class=\"mt-10\">\n <a href=\"#0\">\n <img src=\"img\/feed.svg\" alt=\"\">\n <\/a>\n <\/div>\n <\/div>\n\n <div class=\"w-full md:w-1\/3 lg:w-1\/4 flex-none md:px-6\">\n <div class=\"w-full md:w-1\/3 lg:w-1\/4 flex-none md:px-6\">\n <div class=\"p-4\" style=\"background-color: #f6f6f2\">\n <h2 class=\"font-serif font-normal text-base text-gray-900 border-b border-solid border-gray-300 mb-3\">Search<\/h2>\n\n <div>\n <form action=\"#\" class=\"flex\">\n <input type=\"text\" class=\"border border-solid border-gray p-2 w-full xl:w-auto\">\n\n <button type=\"submit\" class=\"bg-gray-300 px-3 rounded-full border-b border-solid border-gray-600 ml-2 flex-none\" style=\"background-color: #f0f0f0\">\n <img src=\"img\/loupe.svg\" class=\"block\">\n <\/button>\n <\/form>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n\n <div id=\"footer\" class=\"text-xs text-white\">\n <div class=\"container mx-auto px-4 pt-16 pb-4\">\n <div class=\"border-t border-solid border-gray-900 pt-6 -mb-6\">\n <div class=\"mb-6\">\n <p><a href=\"#0\">Contact<\/a><\/p>\n <\/div>\n\n <div class=\"mb-6\">\n <p>\n A clone of <a href=\"https:\/\/www.drupal.org\">Drupal<\/a>\u2019s default theme (Bartik).\n Built by <a href=\"https:\/\/www.oliverdavies.uk\">Oliver Davies<\/a>\n using <a href=\"https:\/\/vuejs.org\">Vue.js<\/a>\n and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a>.\n <\/p>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/template>\n\n<script>\nimport MainMenu from '.\/MainMenu.vue';\n\nexport default {\n components: {\n MainMenu,\n },\n\n props: {\n title: {\n type: String,\n required: true\n }\n }\n}\n<\/script>\n<\/code><\/pre>\n\n<p><\/p>\n\n<\/div>\n\n<h2 id=\"making-it-responsive\">Making it responsive<\/h2>\n\n<p>The second stage began with making the existing desktop version responsive - particularly making the navigation menu behave and appear differently on mobile and tablet screens, and stacking the main content area and the sidebar on mobile screens. This was all achieved using Tailwind\u2019s responsive variants.<\/p>\n\n<pre><code class=\"html\"><div class=\"bg-white pt-3 pb-4 lg:pb-12\">\n ...\n<\/div>\n<\/code><\/pre>\n\n<p>In this example, the <code>pb-4<\/code> class adds 1rem of bottom padding to the element by default, then increases it to 3rem at large screen sizes due to the <code>lg:pb-12<\/code> class.<\/p>\n\n<h2 id=\"adding-interactivity\">Adding interactivity<\/h2>\n\n<p>This is how the main navigation menu works on mobile:<\/p>\n\n<p><img src=\"\/images\/blog\/rebuilding-bartik-vue-tailwind\/rebuilt-mobile.png\" alt=\"The main navigation menu on mobile.\" \/><\/p>\n\n<p>The show and hide text appears next to a hamburger menu, and clicking it toggles the visiblity of the menu links which are stacked below, as well as the wording of the text itself.<\/p>\n\n<p>The code for this was moved into a separate <code>MainMenu<\/code> component, which means that it was easier to have dedicated data properties for whether the menu was open or not, as well as computed properties for building the show\/hide text. The <code>open<\/code> value can then be used to apply the appropriate classes to the main menu to toggle it.<\/p>\n\n<p>I also moved the links into <code>data<\/code> too - each link is it\u2019s own object with it's <code>title<\/code> and <code>href<\/code> values. This means that I can use a <code>v-for<\/code> directive to loop over the data items and inject dynamic values, removing the duplication of markup which makes the component easier to read and maintain.<\/p>\n\n<p><code>src\/components\/MainMenu.vue<\/code>:<\/p>\n\n<p><\/p>\n\n<div v-pre>\n\n<pre><code class=\"vuejs\"><template>\n <div>\n <button\n type=\"button\"\n class=\"w-full p-3 block sm:hidden bg-blue-light text-sm text-gray-800 text-left focus:outline-none\"\n @click=\"open = !open\"\n >\n <div class=\"flex items-center justify-between\">\n <div>\n {{ navText }} - Main navigation\n <\/div>\n <div>\n <img src=\"img\/hamburger.svg\" alt=\"\">\n <\/div>\n <\/div>\n <\/button>\n\n <div class=\"container mx-auto px-4 sm:block\" :class=\"[ open ? 'block' : 'hidden' ]\">\n <div class=\"mt-2 sm:mt-0\">\n <nav class=\"flex flex-wrap pb-1 md:p-0 -mx-3 sm:-mx-0\">\n <div\n class=\"px-1 sm:pl-0 mb-1 md:mb-0 inline-block w-full sm:w-1\/3 md:w-auto\"\n :key=\"link.title\"\n v-for=\"(link, index) in links\"\n >\n <a\n class=\"block text-sm no-underline text-black px-3 py-2 rounded-lg md:rounded-none md:rounded-t-lg sm:text-center\"\n :class=\"[ index == activeTab ? 'bg-white' : 'bg-blue-light hover:bg-white' ]\"\n :href=\"link.href\"\n >\n {{ link.title }}\n <\/a>\n <\/div>\n <\/nav>\n <\/div>\n <\/div>\n <\/div>\n<\/template>\n\n<script>\n export default {\n data: function () {\n return {\n activeTab: 0,\n open: false,\n links: [\n {\n title: 'Home',\n href: '#0',\n },\n {\n title: 'Drupal',\n href: 'https:\/\/www.drupal.org',\n },\n {\n title: 'Vue.js',\n href: 'https:\/\/vuejs.org',\n },\n {\n title: 'Tailwind CSS',\n href: 'https:\/\/tailwindcss.com',\n },\n {\n title: 'View code on GitHub',\n href: 'https:\/\/github.com\/opdavies\/rebuilding-bartik',\n },\n {\n title: 'Read blog post',\n href: 'https:\/\/www.oliverdavies.uk\/blog\/rebuilding-bartik-with-vuejs-tailwind-css',\n },\n ]\n }\n },\n\n computed: {\n navText: function () {\n return this.open ? 'Hide' : 'Show';\n }\n }\n }\n<\/script>\n<\/code><\/pre>\n\n<\/div>\n\n<p><\/p>\n\n<h2 id=\"the-result\">The result<\/h2>\n\n<p>The whole task only took around two hours to complete, and although some of the colours and spacings are slightly different due to the decision to stick with the default Tailwind configuration values, I\u2019m happy with the result.<\/p>\n\n<h3 id=\"the-original-version\">The original version<\/h3>\n\n<p><img src=\"\/images\/blog\/rebuilding-bartik-vue-tailwind\/original.png\" alt=\"The original Bartik theme in a new installation of Drupal 8\" \/><\/p>\n\n<h3 id=\"the-vue.js-and-tailwind-css-version\">The Vue.js and Tailwind CSS version<\/h3>\n\n<p><img src=\"\/images\/blog\/rebuilding-bartik-vue-tailwind\/rebuilt-desktop.png\" alt=\"The Vue.js\/Tailwind CSS version, hosted on Netlify\" \/><\/p>\n\n<div class=\"note\">\n\n<p>I\u2019ve also made some additional changes since this version, which are described in <a href=\"\/blog\/rebuilding-bartik-with-vuejs-tailwind-css-part-2\">this follow-up post<\/a>.<\/p>\n\n<\/div>\n",
|
||
"tags": ["drupal"
|
||
,"tailwind-css"
|
||
,"tweet"
|
||
,"vuejs"
|
||
]
|
||
}, {
|
||
"title": "Debugging Drupal Commerce Promotions and Adjustments using Illuminate Collections (Drupal 8)",
|
||
"path": "/articles/debugging-drupal-commerce-promotions-illiminate-collections",
|
||
"is_draft": "false",
|
||
"created": "1540339200",
|
||
"excerpt": "Using Laravel\u2019s Illuminate Collections to debug an issue with a Drupal Commerce promotion.",
|
||
"body": "<p>Today I found another instance where I decided to use <a href=\"https:\/\/laravel.com\/docs\/collections\">Illuminate Collections<\/a> within my Drupal 8 code; whilst I was debugging an issue where a <a href=\"https:\/\/drupalcommerce.org\">Drupal Commerce<\/a> promotion was incorrectly being applied to an order.<\/p>\n\n<p>No adjustments were showing in the Drupal UI for that order, so after some initial investigation and finding that <code>$order->getAdjustments()<\/code> was empty, I determined that I would need to get the adjustments from each order item within the order.<\/p>\n\n<p>If the order were an array, this is how it would be structured in this situation:<\/p>\n\n<pre><code class=\"php\">$order = [\n 'id' => 1,\n 'items' => [\n [\n 'id' => 1,\n 'adjustments' => [\n ['name' => 'Adjustment 1'], \n ['name' => 'Adjustment 2'], \n ['name' => 'Adjustment 3'],\n ]\n ],\n [\n 'id' => 2,\n 'adjustments' => [\n ['name' => 'Adjustment 4'],\n ]\n ],\n [\n 'id' => 3,\n 'adjustments' => [\n ['name' => 'Adjustment 5'], \n ['name' => 'Adjustment 6'],\n ]\n ],\n ],\n];\n<\/code><\/pre>\n\n<h2 id=\"getting-the-order-items\">Getting the order items<\/h2>\n\n<p>I started by using <code>$order->getItems()<\/code> to load the order\u2019s items, converted them into a Collection, and used the Collection\u2019s <code>pipe()<\/code> method and the <code>dump()<\/code> function provided by the <a href=\"https:\/\/www.drupal.org\/project\/devel\">Devel module<\/a> to output the order items.<\/p>\n\n<pre><code class=\"php\">collect($order->getItems())\n ->pipe(function (Collection $collection) {\n dump($collection);\n });\n<\/code><\/pre>\n\n<h2 id=\"get-the-order-item-adjustments\">Get the order item adjustments<\/h2>\n\n<p>Now we have a Collection of order items, for each item we need to get it\u2019s adjustments. We can do this with <code>map()<\/code>, then call <code>getAdjustments()<\/code> on the order item.<\/p>\n\n<p>This would return a Collection of arrays, with each array containing it\u2019s own adjustments, so we can use <code>flatten()<\/code> to collapse all the adjustments into one single-dimensional array.<\/p>\n\n<pre><code class=\"php\">collect($order->getItems())\n ->map(function (OrderItem $order_item) {\n return $order_item->getAdjustments();\n })\n ->flatten(1);\n<\/code><\/pre>\n\n<p>There are a couple of refactors that we can do here though:<\/p>\n\n<ul>\n<li>Use <code>flatMap()<\/code> to combine the <code>flatten()<\/code> and <code>map()<\/code> methods.<\/li>\n<li>Use <a href=\"https:\/\/laravel-news.com\/higher-order-messaging\">higher order messages<\/a> to delegate straight to the <code>getAdjustments()<\/code> method on the order, rather than having to create a closure and call the method within it.<\/li>\n<\/ul>\n\n<pre><code class=\"php\">collect($order->getItems())\n ->flatMap->getAdjustments();\n<\/code><\/pre>\n\n<h2 id=\"filtering\">Filtering<\/h2>\n\n<p>In this scenario, each order item had three adjustments - the correct promotion, the incorrect one and the standard VAT addition.\nI wasn\u2019t concerned about the VAT adjustment for debugging, so I used <code>filter()<\/code> to remove it based on the result of the adjustment\u2019s <code>getSourceId()<\/code> method.<\/p>\n\n<pre><code class=\"php\">collect($order->getItems())\n ->flatMap->getAdjustments()\n ->filter(function (Adjustment $adjustment) {\n return $adjustment->getSourceId() != 'vat';\n });\n<\/code><\/pre>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>Now I have just the relevant adjustments, I want to be able to load each one to load it and check it\u2019s conditions. To do this, I need just the source IDs.<\/p>\n\n<p>Again, I can use a higher order message to directly call <code>getSourceId()<\/code> on the adjustment and return it\u2019s value to <code>map()<\/code>.<\/p>\n\n<pre><code class=\"php\">collect($order->getItems())\n ->flatMap->getAdjustments()\n ->filter(function (Adjustment $adjustment) {\n return $adjustment->getSourceId() != 'vat';\n })\n ->map->getSourceId();\n<\/code><\/pre>\n\n<p>This returns a Collection containing just the relevant promotion IDs being applied to the order that I can use for debugging.<\/p>\n\n<p>Now just to find out why the incorrect promotion was applying!<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-8"
|
||
,"drupal-commerce"
|
||
,"drupal-planet"
|
||
,"illuminate-collections"
|
||
,"laravel-collections"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Quick Project Switching in PhpStorm",
|
||
"path": "/articles/quick-project-switching-in-phpstorm",
|
||
"is_draft": "false",
|
||
"created": "1536019200",
|
||
"excerpt": "How to quickly switch between projects in PhpStorm.",
|
||
"body": "<p>Following a recent conversation on Twitter with <a href=\"https:\/\/twitter.com\/socketwench\">socketwench<\/a> about project switching in PhpStorm, I thought I\u2019d document my workflow here.<\/p>\n\n<p>Here is the original tweet and my initial response. I also have a lot of PhpStorm projects, and as I\u2019m always working on multiple projects I regularly need to switch between them.<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<p lang=\"en\" dir=\"ltr\">I think you can start typing and it will filter?<\/p>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1034472920532365312?ref_src=twsrc%5Etfw\">August 28, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>On the PhpStorm welcome screen that displays when you first open it, your recent projects are displayed on the left-hand side of the screen, and are filterable. That means that I can start typing a project name, e.g. <code>oli<\/code>, and I will only see projects that start with that input.<\/p>\n\n<p><img src=\"\/images\/blog\/quick-project-switching-phpstorm\/welcome-screen.png\" alt=\"The PhpStorm welcome screen with filters applied to the project list\" title=\"\" class=\"with-border with-padding\" \/><\/p>\n\n<p>That\u2019s great when opening a project from scratch, but what about when we\u2019re already within a project and just want to be able to switch to another?<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<p lang=\"en\" dir=\"ltr\">You can also use 'Open recent' within the actions list, and then filter the list of projects. <a href=\"https:\/\/t.co\/k8G9iIQNP0\">pic.twitter.com\/k8G9iIQNP0<\/a><\/p>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1034542753651281920?ref_src=twsrc%5Etfw\">August 28, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>There\u2019s also a way to access this list once PhpStorm is open, by clicking 'Open Recent' within the File menu. The issue here though is that this list is not filterable.<\/p>\n\n<p>You can also access this list using the keyboard, though the 'Search everywhere' or 'Find action' panes, and these are filterable.<\/p>\n\n<p><img src=\"\/images\/blog\/quick-project-switching-phpstorm\/find-action.png\" alt=\"Using the 'find action' pane to find 'Open Recent'\" title=\"\" class=\"with-border with-padding\" \/><\/p>\n\n<p>Once the 'Open Recent' option is selected, you see the same project list as on the welcome screen, which is filtered in the same way by starting to type potential project names.<\/p>\n\n<p><img src=\"\/images\/blog\/quick-project-switching-phpstorm\/open-recent.png\" alt=\"The filtered project list\" title=\"\" class=\"with-border with-padding\" \/><\/p>\n\n<h2 id=\"adding-a-keyboard-shortcut\">Adding a Keyboard Shortcut<\/h2>\n\n<p>We can make this easier by adding a new keyboard shortcut. Within the Keymap preferences, you can search for 'Open Recent' and right-click it to add a new keyboard shortcut and define the key combination.<\/p>\n\n<p><img src=\"\/images\/blog\/quick-project-switching-phpstorm\/adding-keyboard-shortcut-1.png\" alt=\"Finding the 'Open Recent' shortcut in the Keymap preferences\" title=\"\" class=\"with-border with-padding\" \/><\/p>\n\n<p><img src=\"\/images\/blog\/quick-project-switching-phpstorm\/adding-keyboard-shortcut-2.png\" alt=\"Assigning a keyboard shortcut\" title=\"\" class=\"with-border with-padding\" \/><\/p>\n\n<p>This this shortcut added, you can now use it to instantly bring up your recent projects list, filter it and switch project.<\/p>\n",
|
||
"tags": ["phpstorm"
|
||
]
|
||
}, {
|
||
"title": "Examples of using Laravel Collections in Drupal",
|
||
"path": "/articles/examples-of-laravel-collections-in-drupal",
|
||
"is_draft": "false",
|
||
"created": "1534982400",
|
||
"excerpt": "Some examples of using Laravel\u2019s Illuminate Collections within Drupal projects.",
|
||
"body": "<p>Since starting to work with Laravel as well as Drupal and Symfony, watching Adam Wathan\u2019s <a href=\"https:\/\/adamwathan.me\/refactoring-to-collections\">Refactoring to Collections<\/a> course as well as <a href=\"https:\/\/laracasts.com\/series\/how-do-i\/episodes\/18\">lessons on Laracasts<\/a>, I\u2019ve become a fan of <a href=\"https:\/\/laravel.com\/docs\/collections\">Laravel\u2019s Illuminate Collections<\/a> and the object-orientated pipeline approach for interacting with PHP arrays.<\/p>\n\n<p>In fact I\u2019ve given a talk on <a href=\"\/talks\/using-laravel-collections-outside-laravel\">using Collections outside Laravel<\/a> and have written a <a href=\"https:\/\/www.drupal.org\/project\/collection_class\">Collection class module<\/a> for Drupal 7.<\/p>\n\n<p>I\u2019ve also tweeted several examples of code that I\u2019ve written within Drupal that use Collections, and I thought it would be good to collate them all here for reference.<\/p>\n\n<p>Thanks again to <a href=\"https:\/\/tighten.co\">Tighten<\/a> for releasing and maintaining the <a href=\"https:\/\/packagist.org\/packages\/tightenco\/collect\">tightenco\/collect library<\/a> that makes it possible to pull in Collections via Composer.<\/p>\n\n<div class=\"lg:flex lg:flex-wrap lg:-mx-4\">\n <div class=\"my-4 flex justify-center block mb-4 lg:w-1\/2 lg:px-2 lg:mb-0\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <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>\n <\/blockquote>\n<\/div>\n\n <div class=\"my-4 flex justify-center block mb-4 lg:w-1\/2 lg:px-2 lg:mb-0\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <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. \ud83d\ude01 <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>\n <\/blockquote>\n<\/div>\n\n <div class=\"my-4 flex justify-center block mb-4 lg:w-1\/2 lg:px-2 lg:mb-0\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <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! \ud83d\ude0e <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>\n <\/blockquote>\n<\/div>\n\n <div class=\"my-4 flex justify-center block mb-4 lg:w-1\/2 lg:px-2 lg:mb-0\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <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>\n <\/blockquote>\n<\/div>\n\n <div class=\"my-4 flex justify-center block mb-4 lg:w-1\/2 lg:px-2 lg:mb-0\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <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>\n <\/blockquote>\n<\/div>\n<\/div>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-8"
|
||
,"drupal-planet"
|
||
,"laravel"
|
||
,"laravel-collections"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Experimenting with events in Drupal 8",
|
||
"path": "/articles/experimenting-with-events-in-drupal-8",
|
||
"is_draft": "false",
|
||
"created": "1534809600",
|
||
"excerpt": "Trying a different way of structuring Drupal modules, using event subscribers and autowiring.",
|
||
"body": "<p>I\u2019ve been experimenting with moving some code to Drupal 8, and I\u2019m quite intrigued by a different way that I\u2019ve tried to structure it - using event subscribers, building on some of the takeaways from Drupal Dev Days.<\/p>\n\n<p>Here is how this module is currently structured:<\/p>\n\n<p><img src=\"\/images\/blog\/events-drupal-8\/1.png\" alt=\"\" title=\"\" class=\"border p-1\" \/><\/p>\n\n<p>Note that there is no <code>opdavies_blog.module<\/code> file, and rather than calling actions from within a hook like <code>opdavies_blog_entity_update()<\/code>, each action becomes it\u2019s own event subscriber class.<\/p>\n\n<p>This means that there are no long <code>hook_entity_update<\/code> functions, and instead there are descriptive, readable event subscriber class names, simpler action code that is responsibile only for performing one task, and you\u2019re 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.<\/p>\n\n<p>The additional events are provided by the <a href=\"https:\/\/www.drupal.org\/project\/hook_event_dispatcher\">Hook Event Dispatcher module<\/a>.<\/p>\n\n<h2 id=\"code\">Code<\/h2>\n\n<p><code>opdavies_blog.services.yml<\/code>:<\/p>\n\n<pre><code class=\"yaml\">services:\n Drupal\\opdavies_blog\\EventSubscriber\\PostToMedium:\n autowire: true\n tags:\n - { name: event_subscriber }\n\n Drupal\\opdavies_blog\\EventSubscriber\\SendTweet:\n autowire: true\n tags:\n - { name: event_subscriber }\n<\/code><\/pre>\n\n<div class=\"note\">\n\n<p>Adding <code>autowire: true<\/code> is not required for the event subscriber to work. I\u2019m using it to automatically inject any dependencies into the class rather than specifying them separately as arguments.<\/p>\n\n<\/div>\n\n<p><code>src\/EventSubscriber\/SendTweet.php<\/code>:<\/p>\n\n<pre><code class=\"php\">namespace Drupal\\opdavies_blog\\EventSubscriber;\n\nuse Drupal\\hook_event_dispatcher\\Event\\Entity\\EntityUpdateEvent;\nuse Drupal\\hook_event_dispatcher\\HookEventDispatcherInterface;\nuse Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\n\nclass SendTweet implements EventSubscriberInterface {\n\n ...\n\n public static function getSubscribedEvents() {\n return [\n HookEventDispatcherInterface::ENTITY_UPDATE => 'sendTweet',\n ];\n }\n\n public function sendTweet(EntityUpdateEvent $event) {\n \/\/ Perform checks and send the tweet.\n }\n\n}\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-8"
|
||
,"drupal-planet"
|
||
,"php"
|
||
,"symfony"
|
||
]
|
||
}, {
|
||
"title": "Null Users and System Users in Drupal",
|
||
"path": "/articles/null-users-and-system-users-in-drupal",
|
||
"is_draft": "false",
|
||
"created": "1534377600",
|
||
"excerpt": "Announcing the Null User and System User modules.",
|
||
"body": "<p>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?<\/p>\n\n<p>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\u2019s 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.<\/p>\n\n<p>I had to do this recently, and rather than just duplicate the code I decided to make it into it\u2019s own module - which then became two modules.<\/p>\n\n<h2 id=\"system-users\">System users<\/h2>\n\n<p>The <a href=\"https:\/\/www.drupal.org\/project\/system_user\">System User module<\/a> 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.<\/p>\n\n<p>'System user' is a term used in Linux, which I thought also applies well to this scenario.<\/p>\n\n<p>From <a href=\"https:\/\/www.ssh.com\/iam\/user\/system-account\">https:\/\/www.ssh.com\/iam\/user\/system-account<\/a>:<\/p>\n\n<blockquote>\n <p>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.<\/p>\n<\/blockquote>\n\n<p>A system user isn\u2019t an account that we\u2019d 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.<\/p>\n\n<h3 id=\"declaring-a-user-as-a-system-user\">Declaring a user as a system user<\/h3>\n\n<p>System User module adds a base field to Drupal\u2019s User entity, which determines whether or not each user is a system user - i.e. if this field is <code>TRUE<\/code>, 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.<\/p>\n\n<p><img src=\"\/images\/blog\/null-users-system-users\/drupal-8-users-field-data-table.png\" alt=\"\" title=\"\" class=\"border p-1\" \/><\/p>\n\n<p>In the Drupal 8 version of the module, a <code>SystemUser<\/code> is a custom entity, that contains it\u2019s own <code>create<\/code> method for creating new system users. This is a essentially a wrapper around <code>User::create()<\/code> that automatically sets the value of the system user field as part of the creation.<\/p>\n\n<p>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\u2019ve 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.<\/p>\n\n<p>There is also an open issue to add a Drush command to create a new system user, and I\u2019d imagine I\u2019ll also add a Drupal Console command too.<\/p>\n\n<h3 id=\"retrieving-system-users\">Retrieving system users<\/h3>\n\n<p>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 <code>SystemUserManager<\/code> 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.<\/p>\n\n<pre><code>\/\/ Retrieve the first system user.\n$system_user = $this->systemUserManager->getFirst();\n\n\/\/ Is the specified user a system user?\n$is_system_user = SystemUserManager::isSystemUser($user);\n<\/code><\/pre>\n\n<p>But what do we return if there are no system users?\nYou could return <code>NULL<\/code> or <code>FALSE<\/code>, but I decided to take a different approach, which became the second module.<\/p>\n\n<h2 id=\"null-users\">Null users<\/h2>\n\n<p>The <a href=\"https:\/\/www.drupal.org\/project\/null_user\">Null User module<\/a> is an implementation of the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Null_object_pattern\">null object pattern<\/a> for users in Drupal 8. In this case, a <a href=\"http:\/\/cgit.drupalcode.org\/null_user\/tree\/src\/NullUser.php?h=8.x-1.x\">NullUser<\/a> is an extension of Drupal\u2019s <code>AnonymousUserSession<\/code>, which means that it inherits sensible defaults to return for a non-existent User. Though, through inheritance, the <code>id<\/code>, <code>getRoles<\/code> and <code>hasPermission<\/code> methods are overridden to return relevant values.<\/p>\n\n<pre><code class=\"language-php\">use Drupal\\Core\\Session\\AnonymousUserSession;\n\nclass NullUser extends AnonymousUserSession {\n ...\n}\n<\/code><\/pre>\n\n<p>Null User module is a dependency of System User in Drupal 8, so When no system user is found from the <code>getFirst()<\/code> method, a <code>NullUser<\/code> is returned. Whilst I could alternatively have returned <code>NULL<\/code> or <code>FALSE<\/code>, we then would need to check if the returned value was an object or not before calling methods on it.<\/p>\n\n<pre><code class=\"language-php\">$system_user = $this->systemUserManager->getFirst(); \/\/ Returns NULL or FALSE.\n\n\/\/ Need to check if a user was returned or not.\nif (!$system_user) {\n return;\n}\n\nif ($system_user->isActive()) {\n ...\n}\n<\/code><\/pre>\n\n<p>Because instead we\u2019re returning a <code>NullUser<\/code>, 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.<\/p>\n\n<pre><code class=\"language-php\">$system_user = $this->systemUserManager->getFirst(); \/\/ Returns a NullUser.\n\nif ($system_user->isActive()) {\n ...\n}\n<\/code><\/pre>\n\n<p>This means we have less code, which also is simpler and more readable.<\/p>\n\n<p>System User module is the only one that I\u2019m aware of that makes use of Null User, but I\u2019ve added a list to the <a href=\"https:\/\/www.drupal.org\/project\/null_user\">project page<\/a> so let me know if you can think of any others.<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Null_object_pattern\">Null object pattern<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/project\/null_user\">Null User module<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/project\/system_user\">System User module<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-8"
|
||
,"drupal-modules"
|
||
,"drupal-planet"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Drupal 8 Commerce: Fixing 'No Such Customer' error on checkout",
|
||
"path": "/articles/drupal-8-commerce-fixing-error-on-user-checkout",
|
||
"is_draft": "false",
|
||
"created": "1534291200",
|
||
"excerpt": "Fixing a Drupal Commerce error when a user tries to complete a checkout.",
|
||
"body": "<p>Recently I was experiencing an issue on the Drupal 8 website I\u2019m working on, where a small number of users were not able to complete the checkout process and instead got a generic <code>The site has encountered an unexpected error<\/code> message.<\/p>\n\n<p>Looking at the log, I was able to see the error being thrown (the customer ID has been redacted):<\/p>\n\n<blockquote>\n <p>Stripe\\Error\\InvalidRequest: No such customer: cus_xxxxxxxxxxxxxx in Stripe\\ApiRequestor::_specificAPIError() (line 124 of \/var\/www\/vendor\/stripe\/stripe-php\/lib\/ApiRequestor.php).<\/p>\n<\/blockquote>\n\n<p>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?<\/p>\n\n<h2 id=\"investigation\">Investigation<\/h2>\n\n<p>After some investigation, I found a table in the database named <code>user__commerce_remote_id<\/code> which stores the remote customer ID for each payment method (again, the customer ID has been redacted).<\/p>\n\n<p><img src=\"\/images\/blog\/commerce-stripe-error\/remote-id-table.png\" alt=\"A screenshot of a row in the user__commerce_remote_id table\" title=\"\" class=\"border p-1\" \/><\/p>\n\n<p>The <code>entity_id<\/code> and <code>revision_id<\/code> values in this case refer to the user that the Stripe customer has been associated with.<\/p>\n\n<p>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).<\/p>\n\n<h3 id=\"drupal-code\">Drupal code<\/h3>\n\n<p>This I believe is the Drupal code where the error was being triggered:<\/p>\n\n<pre><code class=\"language-php\">\/\/ modules\/contrib\/commerce_stripe\/src\/Plugin\/Commerce\/PaymentGateway\/Stripe.php\n\npublic function createPayment(PaymentInterface $payment, $capture = TRUE) {\n ...\n\n $owner = $payment_method->getOwner();\n if ($owner && $owner->isAuthenticated()) {\n $transaction_data['customer'] = $this->getRemoteCustomerId($owner);\n }\n\n try {\n $result = \\Stripe\\Charge::create($transaction_data);\n ErrorHelper::handleErrors($result);\n }\n catch (\\Stripe\\Error\\Base $e) {\n ErrorHelper::handleException($e);\n }\n\n ...\n}\n<\/code><\/pre>\n\n<h3 id=\"stripe-code\">Stripe code<\/h3>\n\n<p>I can also see in the Stripe library where the original error is generated.<\/p>\n\n<pre><code class=\"language-php\">private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)\n{\n $msg = isset($errorData['message']) ? $errorData['message'] : null;\n $param = isset($errorData['param']) ? $errorData['param'] : null;\n $code = isset($errorData['code']) ? $errorData['code'] : null;\n\n switch ($rcode) {\n ...\n\n case 404:\n return new Error\\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders);\n\n ...\n }\n}\n<\/code><\/pre>\n\n<h2 id=\"solution\">Solution<\/h2>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-8"
|
||
,"drupal-commerce"
|
||
,"stripe"
|
||
]
|
||
}, {
|
||
"title": "Croeso PHP South Wales!",
|
||
"path": "/articles/croeso-php-south-wales",
|
||
"is_draft": "false",
|
||
"created": "1533081600",
|
||
"excerpt": "Last night was the first meetup of Cardiff\u2019s PHP South Wales user group.",
|
||
"body": "<p>Last night was the first meetup of Cardiff\u2019s <a href=\"https:\/\/www.phpsouthwales.uk\">PHP South Wales user group<\/a>! 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 <a href=\"https:\/\/phpsw.uk\">PHP South West<\/a> regulars.<\/p>\n\n<p>Organised by Steve and Amy McDougall, it was held in Barclays\u2019 <a href=\"https:\/\/labs.uk.barclays\/locations\/cardiff-en\">Eagle Lab<\/a> which was a great space, and it was cool to be back in Brunel House having worked in that building previously whilst at Appnovation.<\/p>\n\n<p><div class=\"my-4 flex justify-center my-6\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <\/p>\n\n<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>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1024377438611156992?ref_src=twsrc%5Etfw\">July 31, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<h2 id=\"speakers\">Speakers<\/h2>\n\n<p><a href=\"https:\/\/twitter.com\/akrabat\">Rob Allen<\/a> 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\u2019ve done a number of times at different events, and it was great to be able to chat for a while after the meetup too.<\/p>\n\n<p><div class=\"my-4 flex justify-center my-6\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <\/p>\n\n<p lang=\"en\" dir=\"ltr\">Great to see \u2066<a href=\"https:\/\/twitter.com\/akrabat?ref_src=twsrc%5Etfw\">@akrabat<\/a>\u2069 speaking about serverless PHP at the first \u2066<a href=\"https:\/\/twitter.com\/phpSouthWales?ref_src=twsrc%5Etfw\">@phpSouthWales<\/a>\u2069 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>\n\n<p>— Oliver Davies (@opdavies) <a href=\"https:\/\/twitter.com\/opdavies\/status\/1024359937063956484?ref_src=twsrc%5Etfw\">July 31, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>We also had a couple of lightning talks, starting with <a href=\"https:\/\/twitter.com\/IsmaelVelasco\">Ismael Velasco<\/a> 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).<\/p>\n\n<p>I gave an updated version of my <a href=\"\/talks\/taking-flight-with-tailwind-css\">Tailwind CSS lightning talk<\/a>, and enjoyed being able to show some examples of new sites using Tailwind such as <a href=\"https:\/\/nova.laravel.com\">Laravel Nova<\/a>, <a href=\"https:\/\/spatie.be\">Spatie<\/a>\u2019s new website and PHP South Wales itself!<\/p>\n\n<p><div class=\"my-4 flex justify-center my-6\">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n >\n <\/p>\n\n<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> \ud83d\ude0e\ud83c\udf89 <a href=\"https:\/\/t.co\/KrJGZlIp7V\">pic.twitter.com\/KrJGZlIp7V<\/a><\/p>\n\n<p>— PHP South Wales (@phpSouthWales) <a href=\"https:\/\/twitter.com\/phpSouthWales\/status\/1024377906456420352?ref_src=twsrc%5Etfw\">July 31, 2018<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>It\u2019s great to have a meetup in Cardiff again, and having thought about organsing something myself previously, I\u2019m 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\u2019ll look forward to seeing the local community grow!<\/p>\n\n<p>Thanks again to Steve and Amy for organising, Eagle Labs for hosting, the sponsors, and Rob and Ismael for speaking.<\/p>\n\n<p>It would be great to see even more people at the next one. If you\u2019re interested, take a look at the <a href=\"https:\/\/www.phpsouthwales.uk\">group\u2019s website<\/a>, <a href=\"https:\/\/www.meetup.com\/PHP-South-Wales\">meetup.com group<\/a> and <a href=\"https:\/\/twitter.com\/phpsouthwales\">Twitter profile<\/a>. Alternatively, get in touch with myself or one of the organisers for more information.<\/p>\n\n<p><strong>Croeso ac iechyd da PHP South Wales!<\/strong><\/p>\n",
|
||
"tags": ["php"
|
||
,"php-south-wales"
|
||
,"meetups"
|
||
]
|
||
}, {
|
||
"title": "How to run Drupal 8 PHPUnit Tests within Docksal from PhpStorm",
|
||
"path": "/articles/running-phpunit-tests-docksal-phpstorm",
|
||
"is_draft": "false",
|
||
"created": "1531958400",
|
||
"excerpt": "How to configure PhpStorm to run automated tests within Docksal.",
|
||
"body": "<p>I\u2019ve recently re-watched <a href=\"https:\/\/laracasts.com\/series\/php-bits\/episodes\/2\">A Clean PHPUnit Workflow in PHPStorm<\/a> on <a href=\"https:\/\/laracasts.com\">Laracasts<\/a>, where Jeffrey configures PhpStorm to run tests from within the IDE. With Drupal 8 using PHPUnit too, I decided to try and do the same with a local D8 site.<\/p>\n\n<p>Though because I\u2019m using <a href=\"https:\/\/docksal.io\">Docksal<\/a> for my local development environment which, at least on a Mac, runs Docker containers within a virtual machine, there were some additional steps needed to achieve this and to have the tests run within the Docksal virtual machine and using the correct containers.<\/p>\n\n<p>In this post, I\u2019ll be using my <a href=\"https:\/\/github.com\/opdavies\/drupal-testing-workshop\">Drupal Testing Workshop codebase<\/a> as an example, which is based on the <a href=\"https:\/\/github.com\/drupal-composer\/drupal-project\">Drupal Composer project<\/a> with some pre-configured Docksal configuration.<\/p>\n\n<p>This post is separated into a few different sections:<\/p>\n\n<ul>\n<li><a href=\"#allow-phpstorm-to-connect-to-the-cli-container\">Allow PhpStorm to connect to the CLI container<\/a><\/li>\n<li><a href=\"#add-a-new-deployment-server\">Add a new deployment server<\/a><\/li>\n<li><a href=\"#configuring-the-php-interpreter\">Configure PHP interpreter<\/a><\/li>\n<li><a href=\"#set-up-phpunit-in-phpstorm\">Set up PHPUnit in PhpStorm<\/a><\/li>\n<li><a href=\"#running-tests\">Running tests<\/a><\/li>\n<\/ul>\n\n<h2 id=\"allow-phpstorm-to-connect-to-the-cli-container\">Allow PhpStorm to connect to the CLI container<\/h2>\n\n<p>The first thing to do is to allow PhpStorm to connect to Docksal\u2019s CLI container to allow it to run the tests. We can do this by exposing the container\u2019s SSH port so that it\u2019s available to the host machine and PhpStorm.<\/p>\n\n<p>As this is going to be unique to my environment, I\u2019m going to add this to <code>.docksal\/docksal-local.yml<\/code> which I have in <code>.gitignore<\/code>, rather than committing it into the repository and enforcing the same port number for everyone else and potentially causing conflicts.<\/p>\n\n<p>In this case I\u2019ll expose port 22 in the container to port 2225 locally.<\/p>\n\n<pre><code>version: '2.1'\n\nservices:\n cli:\n ports:\n - '2225:22'\n<\/code><\/pre>\n\n<p>Once added, run <code>fin start<\/code> to rebuild the project\u2019s containers.<\/p>\n\n<p>You can verify the change by running <code>fin ps<\/code> and you should see something like <code>0.0.0.0:2225->22\/tcp<\/code> under Ports for the CLI container.<\/p>\n\n<h2 id=\"add-a-new-deployment-server\">Add a new Deployment server<\/h2>\n\n<p>Now PhpStorm can connect to Docksal, I can configure it to do so by adding a new deployment server.<\/p>\n\n<ul>\n<li>Open PhpStorm\u2019s preferences, and go to 'Build, Execution, Deployment' and 'Deployment'.<\/li>\n<li>Click 'Add' to configure a new deployment server.<\/li>\n<li>Enter a name like 'Docksal', and select SFTP as the server type.<\/li>\n<\/ul>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/deployment-1.png\" alt=\"Adding a new deployment server\" title=\"\" class=\"with-border sm:max-w-sm\" \/><\/p>\n\n<h3 id=\"connection-settings\">Connection settings<\/h3>\n\n<p>On the Connection tab:<\/p>\n\n<ul>\n<li>Enter your domain name - e.g. <code>drupaltest.docksal<\/code> as the SFTP host. This will resolve to the correct local IP address.<\/li>\n<li>Enter the exposed port for the CLI container that was entered in the previous step.<\/li>\n<li>Enter \"docker\" as both the username and password.<\/li>\n<\/ul>\n\n<p>You should now be able to click \"Test SFTP connection\" and get a successfully connected confirmation message.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/deployment-2.png\" alt=\"Configuring a new deployment server\" \/><\/p>\n\n<h3 id=\"mapping-settings\">Mapping settings<\/h3>\n\n<p>On the Mappings tab, add <code>\/var\/www<\/code> as the deployment path so that PhpStorm is looking in the correct place for the project code.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/deployment-3.png\" alt=\"Add mappings to the deployment server\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h2 id=\"configuring-the-php-interpreter\">Configuring the PHP Interpreter<\/h2>\n\n<p>In Preferences, search for 'PHP' within 'Languages & Frameworks', and add a new CLI interpreter.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/cli-interpreter-1.png\" alt=\"The PHP preferences in PhpStorm\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>In this case I\u2019ve called it 'Docksal PHP 7.1', used the Docksal deployment configuration, and set the path to the PHP executable to <code>\/usr\/local\/bin\/php<\/code> (the same path that we would get if we ran <code>fin run which php<\/code>). You should see both the deployment host URL displayed as well as the remote PHP version and configuration filenames.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/cli-interpreter-2.png\" alt=\"Configuring a new CLI interpreter\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>This can now be selected as the CLI interpreter for this project.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/cli-interpreter-3.png\" alt=\"Selecting the new CLI interpreter in the PHP preferences\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h2 id=\"set-up-phpunit-in-phpstorm\">Set up PHPUnit in PhpStorm<\/h2>\n\n<p>In Preferences, search for 'Test Frameworks' and add a new framework.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/phpunit-1.png\" alt=\"Adding a new test framework (PHPUnit) in PHPStorm\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>Select 'PHPUnit by Remote Interpreter' and then the 'Docksal PHP 7.1' that we created in the last step.<\/p>\n\n<p>Select 'Use Composer autoloader' for the PHPUnit library setting so that PhpStorm uses the version required by Drupal core, and set the path to <code>\/var\/www\/vendor\/autoload.php<\/code>.<\/p>\n\n<p>Also specify the path to the default (phpunit.xml) configuration file. This will depend on how your project is structured, in this case it\u2019s at <code>\/var\/www\/web\/core\/phpunit.xml<\/code>.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/phpunit-4.png\" alt=\"Configuring PHPUnit in PHPstorm\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h2 id=\"running-tests\">Running tests<\/h2>\n\n<p>With PHPUnit configured, next to each test class and method, you can see a green circle (or a red one if the test failed the last run). You can click the circle and select to run that test class or method. You can also right-click directories in the project sidebar to run all of the tests within that directory.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/running-tests-1.png\" alt=\"Running a test within PhpStorm\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>When the tests start running, a new tool window will open that shows you all of the selected tests, how long each test took to run and whether it passed or failed. You can also see the CLI output from PHPUnit itself next to it.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/running-tests-2.png\" alt=\"The tests results being displayed\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>From here, you also have the ability to re-run all of the tests, as well as a single test method or a specific test class.<\/p>\n\n<p>Any test failures are shown here too, and for some failures like differences between two arrays you can use PhpStorm\u2019s internal comparison tools to view the difference rather than needing to do so on the command line.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/test-failure-1.png\" alt=\"Showing a failing test\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/test-failure-2.png\" alt=\"Displaying the difference between two arrays\" title=\"\" class=\"with-border sm:max-w-md\" \/><\/p>\n\n<h3 id=\"keyboard-shortcuts\">Keyboard shortcuts<\/h3>\n\n<p>As per the video, I\u2019ve also added some keyboard shortcuts to my keymap, so I can press \u2318T to run the current test method or class that I\u2019m in, and \u21e7\u2318T to re-run the last test.<\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/keyboard-shortcuts-1.png\" alt=\"Adding a keyboard shortcut to run the current test\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p><img src=\"\/images\/blog\/phpstorm-phpunit-docksal\/keyboard-shortcuts-2.png\" alt=\"Adding a keyboard shortcut to re-run the last test\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h3 id=\"database-issues\">Database issues<\/h3>\n\n<p>When running functional tests that require a database, I was getting a database error like the one below:<\/p>\n\n<blockquote>\n <p>Drupal\\Core\\Installer\\Exception\\InstallerException : Resolve all issues below to continue the installation. For help configuring your database server, see the <a href=\"https:\/\/www.drupal.org\/getting-started\/install\">installation handbook<\/a>, or contact your hosting provider.<\/p>\n<\/blockquote>\n\n<p>In <code>settings.php<\/code>, I check for the presence of <code>\/.dockerenv<\/code> to ensure that we\u2019re inside a Docker container, as well as the presence of a <code>docksal.settings.yml<\/code> file. The latter contains the database credentials for Drupal to connect to the MySQL database.<\/p>\n\n<pre><code class=\"php\">if (file_exists('\/.dockerenv') && file_exists(__DIR__ . '\/docksal.settings.php')) {\n include __DIR__ . '\/docksal.settings.php';\n}\n<\/code><\/pre>\n\n<p>In order to get the tests to run, I had to prevent this file from being loaded during the tests. I can do this by checking that <code>SIMPLETEST_DB<\/code>, an environment variable set in phpunit.xml is not present.<\/p>\n\n<pre><code class=\"php\">\/\/ settings.php\n\nif (file_exists('\/.dockerenv') && file_exists(__DIR__ . '\/docksal.settings.php') && !getenv('SIMPLETEST_DB')) {\n include __DIR__ . '\/docksal.settings.php';\n}\n<\/code><\/pre>\n\n<p>With this extra condition, the database credentials are loaded correctly and the functional tests run properly.<\/p>\n\n<p>Happy testing!<\/p>\n",
|
||
"tags": ["docksal"
|
||
,"drupal"
|
||
,"drupal-8"
|
||
,"phpstorm"
|
||
,"phpunit"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Drupal Bristol Testing Workshop",
|
||
"path": "/articles/drupal-bristol-testing-workshop",
|
||
"is_draft": "false",
|
||
"created": "1530144000",
|
||
"excerpt": "Yesterday evening, I did my first workshop, held at the Drupal Bristol user group.",
|
||
"body": "<p>Yesterday evening, I did <a href=\"https:\/\/groups.drupal.org\/node\/520891\">my first workshop<\/a> (and I believe, the first workshop) held at the <a href=\"https:\/\/www.drupalbristol.org.uk\">Drupal Bristol<\/a> user group. The subject was automated testing with PHPUnit in Drupal 8, in preparation for my talk at <a href=\"http:\/\/lisbon2018.drupaldays.org\">Drupal Developer Days 2018<\/a> next week and to help process some ideas for my <a href=\"\/test-driven-drupal\">testing book<\/a>.<\/p>\n\n<p>Here are some details about what we covered, and some of my thoughts in review.<\/p>\n\n<h2 id=\"local-environment\">Local Environment<\/h2>\n\n<p>Before the meetup, I set up a <a href=\"https:\/\/github.com\/opdavies\/drupal-testing-workshop\">repository on GitHub<\/a> that contains a Composer-based Drupal 8 installation, based on the <a href=\"https:\/\/github.com\/drupal-composer\/drupal-project\">Drupal 8 Composer template<\/a> along with the <a href=\"https:\/\/www.drupal.org\/project\/examples\">Examples module<\/a> (which includes some PHPUnit tests) with a pre-configured <a href=\"https:\/\/docksal.io\">Docksal<\/a> environment to use locally - Docksal being our standard local development environment that we use at Microserve for all of our projects, so something that I\u2019m familiar with using.<\/p>\n\n<p>In addition to the default stack, I added <a href=\"\/articles\/creating-a-custom-phpunit-command-for-docksal\">the PHPUnit add-on that I wrote<\/a> so that it was easier to run tests, <a href=\"\/articles\/using-environment-variables-settings-docksal\">configured settings.php using environment variables<\/a> and added a custom <code>fin init<\/code> 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 <code>git clone<\/code> and <code>fin init<\/code>, and could then run tests straight away using <code>fin phpunit web\/modules\/contrib\/examples\/phpunit_example<\/code>.<\/p>\n\n<h2 id=\"exercises\">Exercises<\/h2>\n\n<p>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.<\/p>\n\n<p>The completed code that I wrote beforehand for these is available in their own <a href=\"https:\/\/github.com\/opdavies\/drupal-testing-workshop-exercises\">GitHub repository<\/a>, including all of the tests as well as the implementation code.<\/p>\n\n<p>Once these exercises were completed, we looked at creating a blog page using test driven development - the example that I use in the <a href=\"\/talks\/tdd-test-driven-drupal\">TDD - Test Driven Drupal talk<\/a>, to give a more real-word scenario. It would have been good to have gone through this as an exercise too, if we\u2019d have had more time.<\/p>\n\n<h2 id=\"wrap-up\">Wrap Up<\/h2>\n\n<p>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 <a href=\"https:\/\/www.drupal.org\/project\/private_message_queue\">Private Message Queue<\/a> and <a href=\"https:\/\/www.drupal.org\/project\/system_user\">System User<\/a> 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\u2019ve written on my work project over the last few months.<\/p>\n\n<h2 id=\"slides\">Slides<\/h2>\n\n<p>I didn\u2019t record this workshop, but I have exported the slides and embedded them below:<\/p>\n\n<p><div class=\"slides\">\n <noscript>**Please enable JavaScript to view slides.**<\/noscript>\n <script\n class=\"speakerdeck-embed\"\n data-id=\"2679401cb2ad421789d372cb8d38e368\"\n data-ratio=\"1.77777777777778\"\n src=\"\/\/speakerdeck.com\/assets\/embed.js\"\n ><\/script>\n<\/div>\n<\/p>\n\n<h2 id=\"thoughts\">Thoughts<\/h2>\n\n<p>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.<\/p>\n\n<p>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\u2019ve found that I can export the slide deck as PNGs or JPGs from Deckset, so I\u2019ll definitely do that next time.<\/p>\n\n<p>I\u2019m giving the <a href=\"\/talks\/tdd-test-driven-drupal\">Test Driven Drupal<\/a> talk at the <a href=\"http:\/\/lisbon2018.drupaldays.org\">Drupal Dev Days conference<\/a> next week, and I\u2019m hoping to give it again at other meetups and events in the UK. If you\u2019d like me to do either at your meetup or event, <a href=\"\/contact\">get in touch<\/a>.<\/p>\n",
|
||
"tags": ["composer"
|
||
,"docksal"
|
||
,"drupal"
|
||
,"drupal-8"
|
||
,"drupal-bristol"
|
||
,"php"
|
||
,"phpunit"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "How to Use Environment Variables for your Drupal Settings with Docksal",
|
||
"path": "/articles/using-environment-variables-settings-docksal",
|
||
"is_draft": "false",
|
||
"created": "1528070400",
|
||
"excerpt": "How to leverage environment variables with Drupal and Docksal.",
|
||
"body": "<p>Within the <a href=\"https:\/\/docksal.readthedocs.io\/en\/master\/advanced\/drupal-settings\">Docksal documentation for Drupal settings<\/a>, the example database settings include hard-coded credentials to connect to the Drupal database. For example, within a <code>settings.php<\/code> file, you could add this:<\/p>\n\n<pre><code class=\"language-php\">$databases['default']['default'] = [\n 'driver' => 'mysql',\n 'host' => 'db',\n 'database' => 'myproject_db',\n 'username' => 'myproject_user',\n 'password' => 'myproject_pass',\n];\n<\/code><\/pre>\n\n<p>Whilst this is fine, it does mean that there is duplication in the codebase as the database credentials can also be added as environment variations within <code>.docksal\/docksal.env<\/code> - this is definitely the case if you want to use a custom database name, for example.<\/p>\n\n<p>Also if one of these values were to change, then Drupal wouldn't be aware of that and would no longer be able to connect to the database.<\/p>\n\n<p>It also means that the file can\u2019t simply be re-used on another project as it contains project-specific credentials.<\/p>\n\n<p>We can improve this by using the environment variables within the settings file.<\/p>\n\n<p>The relevant environment variables are <code>MYSQL_DATABASE<\/code> for the database name, and <code>MYSQL_USER<\/code> and <code>MYSQL_PASSWORD<\/code> for the MySQL username and password. These can be set in <code>.docksal\/docksal.env<\/code>, and will need to be present for this to work.<\/p>\n\n<p>For example:<\/p>\n\n<pre><code>DOCKSAL_STACK=default\nMYSQL_DATABASE=myproject_db\nMYSQL_USER=myproject_user\nMYSQL_PASSWORD=myproject_pass\n<\/code><\/pre>\n\n<p>With these in place, they can be referenced within the settings file using the <code>getenv()<\/code> function.<\/p>\n\n<pre><code>$databases['default']['default'] = [\n 'driver' => 'mysql',\n 'host' => 'db',\n 'database' => getenv('MYSQL_DATABASE'),\n 'username' => getenv('MYSQL_USER'),\n 'password' => getenv('MYSQL_PASSWORD'),\n];\n<\/code><\/pre>\n\n<p>Now the credentials are no longer duplicated, and the latest values from the environment variables will always be used.<\/p>\n\n<p>However, you may see a message like this when you try and load the site:<\/p>\n\n<blockquote>\n <p>Drupal\\Core\\Database\\DatabaseAccessDeniedException: SQLSTATE[HY000] [1045] Access denied for user ''@'172.19.0.4' (using password: NO) in \/var\/www\/core\/lib\/Drupal\/Core\/Database\/Driver\/mysql\/Connection.php on line 156<\/p>\n<\/blockquote>\n\n<p>If you see this, the environment variables aren\u2019t being passed into Docksal\u2019s <code>cli<\/code> container, so the values are not being populated. To enable them, edit <code>.docksal\/docksal.yml<\/code> and add <code>MYSQL_DATABASE<\/code>, <code>MYSQL_PASSWORD<\/code> and <code>MYSQL_USER<\/code> to the <code>environment<\/code> section of the <code>cli<\/code> service.<\/p>\n\n<pre><code class=\"language-yml\">version: '2.1'\nservices:\n cli:\n environment:\n - MYSQL_DATABASE\n - MYSQL_PASSWORD\n - MYSQL_USER\n<\/code><\/pre>\n\n<p>After changing this file, run <code>fin start<\/code> to rebuild the project containers and try to load the site again.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"docksal"
|
||
]
|
||
}, {
|
||
"title": "Creating a Custom PHPUnit Command for Docksal",
|
||
"path": "/articles/creating-custom-docksal-commands",
|
||
"is_draft": "false",
|
||
"created": "1525564800",
|
||
"excerpt": "How to write custom commands for Docksal, including one to easily run PHPUnit tests in Drupal 8.",
|
||
"body": "<p>This week I\u2019ve 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.<\/p>\n\n<h2 id=\"what-is-docksal%3F\">What is Docksal?<\/h2>\n\n<p>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 <a href=\"https:\/\/microserve.io\">Microserve<\/a>.<\/p>\n\n<p>There was a <a href=\"https:\/\/youtu.be\/1sjsvnx1P7g\">great talk<\/a> recently at Drupaldelphia about Docksal.<\/p>\n\n<h2 id=\"why-write-a-custom-command%3F\">Why write a custom command?<\/h2>\n\n<p>One of the things that Docksal offers (and is covered in the talk) is the ability to add custom commands to the Docksal\u2019s <code>fin<\/code> CLI, either globally or as part of your project.<\/p>\n\n<p>As an advocate of automated testing and TDD practitioner, I write a lot of tests and run PHPUnit numerous times a day. I\u2019ve also given <a href=\"\/talks\/tdd-test-driven-drupal\">talks<\/a> and have <a href=\"\/articles\/tags\/testing\">written other posts<\/a> on this site relating to testing in Drupal.<\/p>\n\n<p>There are a couple of ways to run PHPUnit with Docksal. The first is to use <code>fin bash<\/code> to open a shell into the container, move into the docroot directory if needed, and run the <code>phpunit<\/code> command.<\/p>\n\n<pre><code class=\"bash\">fin bash\ncd \/var\/www\/docroot\n..\/vendor\/bin\/phpunit -c core modules\/custom\n<\/code><\/pre>\n\n<p>Alternatively, it can be run from the host machine using <code>fin exec<\/code>.<\/p>\n\n<pre><code>cd docroot\nfin exec '..\/vendor\/bin\/phpunit -c core modules\/custom'\n<\/code><\/pre>\n\n<p>Both of these options require multiple steps as we need to be in the <code>docroot<\/code> 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.<\/p>\n\n<p>By adding a custom command, I intend to:<\/p>\n\n<ol>\n<li>Make it easier to get set up to run PHPUnit tests - i.e. setting up a <code>phpunit.xml<\/code> file.<\/li>\n<li>Make it easier to run the tests that we\u2019d written by shortening the command and making it so it can be run anywhere within our project.<\/li>\n<\/ol>\n\n<p>I also hoped to make it project agnostic so that I could add it onto any project and immediately run it.<\/p>\n\n<h2 id=\"creating-the-command\">Creating the command<\/h2>\n\n<p>Each command is a file located within the <code>.docksal\/commands<\/code> directory. The filename is the name of the command (e.g. <code>phpunit<\/code>) with no file extension.<\/p>\n\n<p>To create the file, run this from the same directory where your <code>.docksal<\/code> directory is:<\/p>\n\n<pre><code class=\"bash\">mkdir -p .docksal\/commands\ntouch .docksal\/commands\/phpunit\n<\/code><\/pre>\n\n<p>This will create a new, empty <code>.docksal\/commands\/phpunit<\/code> file, and now the <code>phpunit<\/code> command is now listed under \"Custom commands\" when we run <code>fin<\/code>.<\/p>\n\n<p><img src=\"\/images\/blog\/docksal-phpunit-command\/1.gif\" alt=\"\" \/><\/p>\n\n<p>You can write commands with any interpreter. I\u2019m going to use bash, so I\u2019ll add the shebang to the top of the file.<\/p>\n\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n<\/code><\/pre>\n\n<p>With this in place, I can now run <code>fin phpunit<\/code>, though there is no output displayed or actions performed as the rest of the file is empty.<\/p>\n\n<h2 id=\"adding-a-description-and-help-text\">Adding a description and help text<\/h2>\n\n<p>Currently the description for our command when we run <code>fin<\/code> is the default \"No description\" text. I\u2019d like to add something more relevant, so I\u2019ll start by adding a new description.<\/p>\n\n<p>fin interprets lines starting with <code>##<\/code> as documentation - the first of which it uses as the description.<\/p>\n\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\n## Run automated PHPUnit tests.\n<\/code><\/pre>\n\n<p>Now when I run it, I see the new description.<\/p>\n\n<p><img src=\"\/images\/blog\/docksal-phpunit-command\/2.gif\" alt=\"\" \/><\/p>\n\n<p>Any additional lines are used as help text with running <code>fin help phpunit<\/code>. Here I\u2019ll add an example command to demonstrate how to run it as well as some more in-depth text about what the command will do.<\/p>\n\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\n## Run automated PHPUnit tests.\n##\n## Usage: fin phpunit <args>\n##\n## If a core\/phpunit.xml file does not exist, copy one from elsewhere.\n## Then run the tests.\n<\/code><\/pre>\n\n<p>Now when I run <code>fin help phpunit<\/code>, I see the new help text.<\/p>\n\n<p><img src=\"\/images\/blog\/docksal-phpunit-command\/3.gif\" alt=\"\" \/><\/p>\n\n<h2 id=\"adding-some-content\">Adding some content<\/h2>\n\n<h3 id=\"setting-the-target\">Setting the target<\/h3>\n\n<p>As I want the commands to be run within Docksal\u2019s \"cli\" container, I can specify that with <code>exec_target<\/code>. If one isn\u2019t specified, the commands are run locally on the host machine.<\/p>\n\n<pre><code>#: exec_target = cli\n<\/code><\/pre>\n\n<h3 id=\"available-variables\">Available variables<\/h3>\n\n<p>These variables are provided by fin and are available to use within any custom commands:<\/p>\n\n<ul>\n<li><code>PROJECT_ROOT<\/code> - The absolute path to the nearest <code>.docksal<\/code> directory.<\/li>\n<li><code>DOCROOT<\/code> - name of the docroot folder.<\/li>\n<li><code>VIRTUAL_HOST<\/code> - the virtual host name for the project. Such as <code>myproject.docksal<\/code>.<\/li>\n<li><code>DOCKER_RUNNING<\/code> - (string) \"true\" or \"false\".<\/li>\n<\/ul>\n\n<div class=\"note\">\n\n<p><strong>Note:<\/strong> If the <code>DOCROOT<\/code> variable is not defined within the cli container, ensure that it\u2019s added to the environment variables in <code>.docksal\/docksal.yml<\/code>. For example:<\/p>\n\n<pre><code>version: \"2.1\"\n\nservices:\n cli:\n environment:\n - DOCROOT\n<\/code><\/pre>\n\n<\/div>\n\n<h3 id=\"running-phpunit\">Running phpunit<\/h3>\n\n<p>When you run the <code>phpunit<\/code> command, there are number of options you can pass to it such as <code>--filter<\/code>, <code>--testsuite<\/code> and <code>--group<\/code>, as well as the path to the tests to execute, such as <code>modules\/custom<\/code>.<\/p>\n\n<p>I wanted to still be able to do this by running <code>fin phpunit <args><\/code> so the commands can be customised when executed. However, as the first half of the command (<code>..\/vendor\/bin\/phpunit -c core<\/code>) is consistent, I can wrap that within my custom command and not need to type it every time.<\/p>\n\n<p>By using <code>\"$@\"<\/code> I can capture any additional arguments, such as the test directory path, and append them to the command to execute.<\/p>\n\n<p>I\u2019m using <code>$PROJECT_ROOT<\/code> to prefix the command with the absolute path to <code>phpunit<\/code> so that I don\u2019t need to be in that directory when I run the custom command, and <code>$DOCROOT<\/code> to always enter the sub-directory where Drupal is located. In this case, it\u2019s \"docroot\" though I also use \"web\" and I\u2019ve seen various others used.<\/p>\n\n<pre><code class=\"bash\">DOCROOT_PATH=\"${PROJECT_ROOT}\/${DOCROOT}\"\nDRUPAL_CORE_PATH=\"${DOCROOT_PATH}\/core\"\n\n# If there is no phpunit.xml file, copy one from elsewhere.\n\n# Otherwise run the tests.\n${PROJECT_ROOT}\/vendor\/bin\/phpunit -c ${DRUPAL_CORE_PATH} \"$@\"\n<\/code><\/pre>\n\n<p>For example, <code>fin phpunit modules\/custom<\/code> would execute <code>\/var\/www\/vendor\/bin\/phpunit -c \/var\/www\/docroot\/core modules\/custom<\/code> within the container.<\/p>\n\n<p>I can then wrap this within a condition so that the tests are only run when a <code>phpunit.xml<\/code> file exists, as it is required for them to run successfully.<\/p>\n\n<pre><code class=\"bash\">if [ ! -e ${DRUPAL_CORE_PATH}\/phpunit.xml ]; then\n # If there is no phpunit.xml file, copy one from elsewhere.\nelse\n ${PROJECT_ROOT}\/vendor\/bin\/phpunit -c ${DRUPAL_CORE_PATH} \"$@\"\nfi\n<\/code><\/pre>\n\n<h3 id=\"creating-phpunit.xml---step-1\">Creating phpunit.xml - step 1<\/h3>\n\n<p>My first thought was that if a <code>phpunit.xml<\/code> file doesn\u2019t exist was to duplicate core\u2019s <code>phpunit.xml.dist<\/code> file. However this isn\u2019t enough to run the tests, as values such as <code>SIMPLETEST_BASE_URL<\/code>, <code>SIMPLETEST_DB<\/code> and <code>BROWSERTEST_OUTPUT_DIRECTORY<\/code> need to be populated.<\/p>\n\n<p>As the tests wouldn't run at this point, I\u2019ve exited early and displayed a message to the user to edit the new <code>phpunit.xml<\/code> file and run <code>fin phpunit<\/code> again.<\/p>\n\n<pre><code class=\"bash\">if [ ! -e ${DRUPAL_CORE_PATH}\/phpunit.xml ]; then\n echo \"Copying ${DRUPAL_CORE_PATH}\/phpunit.xml.dist to ${DRUPAL_CORE_PATH}\/phpunit.xml.\"\n echo \"Please edit it's values as needed and re-run 'fin phpunit'.\"\n cp ${DRUPAL_CORE_PATH}\/phpunit.xml.dist ${DRUPAL_CORE_PATH}\/phpunit.xml\n exit 1;\nelse\n ${PROJECT_ROOT}\/vendor\/bin\/phpunit -c ${DRUPAL_CORE_PATH} \"$@\"\nfi\n<\/code><\/pre>\n\n<p>However this isn\u2019t as streamlined as I originally wanted as it still requires the user to perform an additional step before the tests can run.<\/p>\n\n<h3 id=\"creating-phpunit.xml---step-2\">Creating phpunit.xml - step 2<\/h3>\n\n<p>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 <code>.docksal\/drupal\/core\/phpunit.xml<\/code> to be the potential location.<\/p>\n\n<p>Also, if this file is copied then we can go ahead and run the tests straight away rather than needing to exit early.<\/p>\n\n<p>If a pre-configured file doesn\u2019t exist, then we can default back to copying <code>phpunit.xml.dist<\/code>.<\/p>\n\n<p>To avoid duplication, I created a reusable <code>run_tests()<\/code> function so it could be executed in either scenario.<\/p>\n\n<pre><code class=\"bash\">run_tests() {\n ${PROJECT_ROOT}\/vendor\/bin\/phpunit -c ${DRUPAL_CORE_PATH} \"$@\"\n}\n\nif [ ! -e ${DRUPAL_CORE_PATH}\/phpunit.xml ]; then\n if [ -e \"${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml\" ]; then\n echo \"Copying ${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml to ${DRUPAL_CORE_PATH}\/phpunit.xml\"\n cp \"${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml\" ${DRUPAL_CORE_PATH}\/phpunit.xml\n run_tests \"$@\"\n else\n echo \"Copying ${DRUPAL_CORE_PATH}\/phpunit.xml.dist to ${DRUPAL_CORE_PATH}\/phpunit.xml.\"\n echo \"Please edit it's values as needed and re-run 'fin phpunit'.\"\n cp ${DRUPAL_CORE_PATH}\/phpunit.xml.dist ${DRUPAL_CORE_PATH}\/phpunit.xml\n exit 1;\n fi\nelse\n run_tests \"$@\"\nfi\n<\/code><\/pre>\n\n<p>This means that I can execute less steps and run a much shorter command compared to the original, and even if someone didn\u2019t have a <code>phpunit.xml<\/code> file created they could have copied into place and have tests running with only one command.<\/p>\n\n<h2 id=\"the-finished-file\">The finished file<\/h2>\n\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\n#: exec_target = cli\n\n## Run automated PHPUnit tests.\n##\n## Usage: fin phpunit <args>\n##\n## If a core\/phpunit.xml file does not exist, one is copied from\n## .docksal\/core\/phpunit.xml if that file exists, or copied from the default\n## core\/phpunit.xml.dist file.\n\nDOCROOT_PATH=\"${PROJECT_ROOT}\/${DOCROOT}\"\nDRUPAL_CORE_PATH=\"${DOCROOT_PATH}\/core\"\n\nrun_tests() {\n ${PROJECT_ROOT}\/vendor\/bin\/phpunit -c ${DRUPAL_CORE_PATH} \"$@\"\n}\n\nif [ ! -e ${DRUPAL_CORE_PATH}\/phpunit.xml ]; then\n if [ -e \"${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml\" ]; then\n echo \"Copying ${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml to ${DRUPAL_CORE_PATH}\/phpunit.xml\"\n cp \"${PROJECT_ROOT}\/.docksal\/drupal\/core\/phpunit.xml\" ${DRUPAL_CORE_PATH}\/phpunit.xml\n run_tests \"$@\"\n else\n echo \"Copying phpunit.xml.dist to phpunit.xml\"\n echo \"Please edit it's values as needed and re-run 'fin phpunit'.\"\n cp ${DRUPAL_CORE_PATH}\/phpunit.xml.dist ${DRUPAL_CORE_PATH}\/phpunit.xml\n exit 0;\n fi\nelse\n run_tests \"$@\"\nfi\n<\/code><\/pre>\n\n<p>It\u2019s currently available as a <a href=\"https:\/\/gist.github.com\/opdavies\/72611f198ffd2da13f363ea65264b2a5\">GitHub Gist<\/a>, though I\u2019m planning on moving it into a public GitHub repository either on my personal account or the <a href=\"https:\/\/github.com\/microserve-io\">Microserve organisation<\/a>, for people to either use as examples or to download and use directly.<\/p>\n\n<p>I\u2019ve also started to add other commands to projects such as <code>config-export<\/code> 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.<\/p>\n\n<p>I think it\u2019s 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 <code>fin init<\/code> and have it set up everything for your project is very convenient and a big time saver!<\/p>\n\n<div class=\"note\">\n\n<p>Since writing this post, I\u2019ve had a <a href=\"https:\/\/github.com\/docksal\/addons\/pull\/15\">pull request<\/a> accepted for this command to be added as a <a href=\"https:\/\/blog.docksal.io\/installing-addons-in-a-docksal-project-172a6c2d8a5b\">Docksal add-on<\/a>. This means that the command can be added to any Docksal project by running <code>fin addon install phpunit<\/code>. It will be installed into the <code>.docksal\/addons\/phpunit<\/code> directory, and displayed under \"Addons\" rather than \"Custom commands\" when you run <code>fin<\/code>.<\/p>\n\n<\/div>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/phpunit.de\">PHPUnit<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/docs\/8\/phpunit\">PHPUnit in Drupal 8<\/a><\/li>\n<li><a href=\"https:\/\/docksal.io\">Main Docksal website<\/a><\/li>\n<li><a href=\"https:\/\/docksal.readthedocs.io\">Docksal documentation<\/a><\/li>\n<li><a href=\"https:\/\/youtu.be\/1sjsvnx1P7g\">Docksal: one tool to rule local and CI\/CD environments<\/a> - Docksal talk from Drupaldelphia<\/li>\n<li><a href=\"https:\/\/github.com\/docksal\/docksal\/blob\/develop\/examples\/.docksal\/commands\/phpcs\">phpcs example custom command<\/a><\/li>\n<li><a href=\"https:\/\/gist.github.com\/opdavies\/72611f198ffd2da13f363ea65264b2a5\">phpunit command Gist<\/a><\/li>\n<li><a href=\"https:\/\/blog.docksal.io\/installing-addons-in-a-docksal-project-172a6c2d8a5b\">Docksal addons blog post<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/docksal\/addons\">Docksal addons repository<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["docksal"
|
||
,"drupal"
|
||
,"drupal-8"
|
||
,"drupal-planet"
|
||
,"phpunit"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Back to the future with Git’s diff and apply commands",
|
||
"path": "/articles/back-to-the-future-git-diff-apply",
|
||
"is_draft": "false",
|
||
"created": "1524441600",
|
||
"excerpt": "How to revert files using Git, but as a new commit to prevent force pushing.",
|
||
"body": "<p>This is one of those \u201cthere\u2019s probably already a better way to do this\u201d situations, but it worked.<\/p>\n\n<p>I was having some issues this past weekend where, despite everything working fine locally, a server was showing a \u201c500 Internal Server\u201d 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.<\/p>\n\n<p>The <code>git reset<\/code> commands removed the interim commits which meant that I couldn\u2019t push to the remote (force pushing, quite rightly, isn\u2019t allowed for the production branch), and using <code>git revert<\/code> was resulting in merge conflicts in <code>composer.lock<\/code> that I\u2019d rather have avoided if possible.<\/p>\n\n<p>This is what <code>git log --oneline -n 4<\/code> was outputting:<\/p>\n\n<pre><code>14e40bc Change webflo\/drupal-core-require-dev version\nfc058bb Add services.yml\n60bcf33 Update composer.json and re-generate lock file\n722210c More styling\n<\/code><\/pre>\n\n<p><code>722210c<\/code> is the commit SHA that I needed to go back to.<\/p>\n\n<h2 id=\"first-solution\">First Solution<\/h2>\n\n<p>My first solution was to use <code>git diff<\/code> to create a single patch file of all of the changes from the current point back to the original commit. In this case, I\u2019m using <code>head~3<\/code> (four commits before <code>head<\/code>) as the original reference, I could have alternatively used a commit ID, tag or branch name.<\/p>\n\n<pre><code>git diff head head~3 > temp.patch\ngit apply -v temp.patch\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code>rm temp.patch\n\ngit add .\ngit commit -m 'Back to the future'\ngit push\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<h2 id=\"second-solution\">Second Solution<\/h2>\n\n<p>The second solution is just a shorter, cleaner version of the first!<\/p>\n\n<p>Rather than creating a patch file and applying it, the output from <code>git diff<\/code> can be piped straight into <code>git apply<\/code>.<\/p>\n\n<pre><code>git diff head~3 head | git apply -v\n<\/code><\/pre>\n\n<p>This means that there\u2019s only one command to run and no leftover patch file, and I can go ahead and add and commit the changes straight away.<\/p>\n",
|
||
"tags": ["git"
|
||
]
|
||
}, {
|
||
"title": "How to put your PHP application in a subdirectory of another site with Nginx",
|
||
"path": "/articles/php-apps-subdirectory-nginx",
|
||
"is_draft": "false",
|
||
"created": "1520812800",
|
||
"excerpt": "How to configure Nginx to serve a PHP application from within a subdirectory of another.",
|
||
"body": "<p>In January, <a href=\"https:\/\/twitter.com\/fideloper\">Chris Fidao<\/a> posted a video to <a href=\"https:\/\/serversforhackers.com\">Servers for Hackers<\/a> showing how to put different PHP applications in different subdirectories and have them serving on different paths with Nginx. I\u2019ve had to do this a few times previously, and it\u2019s great to have this video as a reference.<\/p>\n\n<blockquote>\n <p>In this video, we work through how to put your PHP application in a subdirectory of another site.<\/p>\n \n <p>For example, we may have an application running at example.org but need a second application running at example.org\/blog.<\/p>\n \n <p>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.<\/p>\n<\/blockquote>\n\n<p><a href=\"https:\/\/serversforhackers.com\/c\/nginx-php-in-subdirectory\">https:\/\/serversforhackers.com\/c\/nginx-php-in-subdirectory<\/a><\/p>\n",
|
||
"tags": ["nginx"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "How to split a new Drupal contrib project from within another repository",
|
||
"path": "/articles/splitting-new-drupal-project-from-repo",
|
||
"is_draft": "false",
|
||
"created": "1520640000",
|
||
"excerpt": "How to use Git to split a directory from within an existing repository into it\u2019s own.",
|
||
"body": "<p>Yay! You\u2019ve written a new Drupal module, theme or installation profile as part of your site, and now you\u2019ve decided to open source it and upload it to Drupal.org as a new contrib project. But how do you split it from the main site repository into it\u2019s own?<\/p>\n\n<p>Well, there are a couple of options.<\/p>\n\n<h2 id=\"does-it-need-to-be-part-of-the-site-repository%3F\">Does it need to be part of the site repository?<\/h2>\n\n<p>An interesting thing to consider is, does it <em>need<\/em> to be a part of the site repository in the first place?<\/p>\n\n<p>If from the beginning you intend to contribute the module, theme or distribution and it\u2019s written as generic and re-usable from the start, then it <em>could<\/em> be created as a separate project on Drupal.org or as a private repository on your Git server from the beginning, and added as a dependency of the main project rather than part of it. It could already have the correct branch name and adhere to the Drupal.org release conventions and be managed as a separate project, then there is no later need to \"clean it up\" or split it from the main repo at all.<\/p>\n\n<p>This is how I worked at the <a href=\"https:\/\/www.drupal.org\/association\">Drupal Association<\/a> - with all of the modules needed for Drupal.org hosted on Drupal.org itself, and managed as a dependency of the site repository with Drush Make.<\/p>\n\n<p>Whether this is a viable option or not will depend on your processes. For example, if your code needs to go through a peer review process before releasing it, then pushing it straight to Drupal.org would either complicate that process or bypass it completely. Pushing it to a separate private repository may depend on your team's level of familiarity with <a href=\"https:\/\/getcomposer.org\">Composer<\/a>, for example.<\/p>\n\n<p>It does though avoid the \u201cwe\u2019ll clean it up and contribute it later\u201d scenario which probably happens less than people intend.<\/p>\n\n<h2 id=\"create-a-new%2C-empty-repository\">Create a new, empty repository<\/h2>\n\n<p>If the project is already in the site repo, this is probably the most common method - to create a new, empty repository for the new project, add everything to it and push it.<\/p>\n\n<p>For example:<\/p>\n\n<pre><code class=\"language-bash\">cd web\/modules\/custom\/my_new_module\n\n# Create a new Git repository.\ngit init\n\n# Add everything and make a new commit.\ngit add -A .\ngit commit -m 'Initial commit'\n\n# Rename the branch.\ngit branch -m 8.x-1.x\n\n# Add the new remote and push everything.\ngit remote add origin username@git.drupal.org:project\/my_new_module.git\ngit push origin 8.x-1.x\n<\/code><\/pre>\n\n<p>There is a huge issue with this approach though - <strong>you now have only one single commit, and you\u2019ve lost the commmit history!<\/strong><\/p>\n\n<p>This means that you lose the story and context of how the project was developed, and what decisions and changes were made during the lifetime of the project so far. Also, if multiple people developed it, now there is only one person being attributed - the one who made the single new commit.<\/p>\n\n<p>Also, if I\u2019m considering adding your module to my project, personally I\u2019m less likely to do so if I only see one \"initial commit\". I\u2019d like to see the activity from the days, weeks or months prior to it being released.<\/p>\n\n<p>What this does allow though is to easily remove references to client names etc before pushing the code.<\/p>\n\n<h2 id=\"use-a-subtree-split\">Use a subtree split<\/h2>\n\n<p>An alternative method is to use <a href=\"https:\/\/github.com\/git\/git\/blob\/master\/contrib\/subtree\/git-subtree.txt\">git-subtree<\/a>, a Git command that \"merges subtrees together and split repository into subtrees\". In this scenario, we can use <code>split<\/code> to take a directory from within the site repo and split it into it\u2019s own separate repository, keeping the commit history intact.<\/p>\n\n<p>Here is the description for the <code>split<\/code> command from the Git project itself:<\/p>\n\n<blockquote>\n <p>Extract a new, synthetic project history from the\n history of the <prefix> subtree. The new history\n includes only the commits (including merges) that\n affected <prefix>, and each of those commits now has the\n contents of <prefix> at the root of the project instead\n of in a subdirectory. Thus, the newly created history\n is suitable for export as a separate git repository.<\/p>\n<\/blockquote>\n\n<div class=\"note\">\n\n<p><strong>Note<\/strong>: This command needs to be run at the top level of the repository. Otherwise you will see an error like \"You need to run this command from the toplevel of the working tree.\".<\/p>\n\n<p>To find the path to the top level, run <code>git rev-parse --show-toplevel<\/code>.<\/p>\n\n<\/div>\n\n<p>In order to do this, you need specify the prefix for the subtree (i.e. the directory that contains the project you\u2019re splitting) as well as a name of a new branch that you want to split onto.<\/p>\n\n<pre><code>git subtree split --prefix web\/modules\/custom\/my_new_module -b split_my_new_module\n<\/code><\/pre>\n\n<p>When complete, you should see a confirmation message showing the branch name and the commit SHA of the branch.<\/p>\n\n<pre><code>Created branch 'split_my_new_module'\n7edcb4b1f4dc34fc3b636b498f4284c7d98c8e4a\n<\/code><\/pre>\n\n<p>If you run <code>git branch<\/code>, you should now be able to see the new branch, and if you run <code>git log --oneline split_my_new_module<\/code>, you should only see commits for that module.<\/p>\n\n<p>If you do need to tidy up a particular commit to remove client references etc, change a commit message or squash some commits together, then you can do that by checking out the new branch, running an interactive rebase and making the required amends.<\/p>\n\n<pre><code>git checkout split_my_new_module\ngit rebase -i --root\n<\/code><\/pre>\n\n<p>Once everything is in the desired state, you can use <code>git push<\/code> to push to the remote repo - specifying the repo URL, the local branch name and the remote branch name:<\/p>\n\n<pre><code>git push username@git.drupal.org:project\/my_new_module.git split_my_new_module:8.x-1.x\n<\/code><\/pre>\n\n<p>In this case, the new branch will be <code>8.x-1.x<\/code>.<\/p>\n\n<p>Here is a screenshot of example module that I\u2019ve split and pushed to GitLab. Notice that there are multiple commits in the history, and each still attributed to it\u2019s original author.<\/p>\n\n<p><img src=\"\/images\/blog\/subtree-split-drupal-module.png\" alt=\"Screenshot of a split project repo on GitLab\" \/><\/p>\n\n<p>Also, as this is standard Git functionality, you can follow the same process to extract PHP libraries, Symfony bundles, WordPress plugins or anything else.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-8"
|
||
,"drupal-planet"
|
||
,"git"
|
||
,"open-source"
|
||
]
|
||
}, {
|
||
"title": "Drupal 8.5.0 Released",
|
||
"path": "/articles/drupal-8-5-released",
|
||
"is_draft": "false",
|
||
"created": "1520553600",
|
||
"excerpt": "This week, the latest version of Drupal 8 was released.",
|
||
"body": "<p>This week the latest minor version of Drupal 8, 8.5.0, was released.<\/p>\n\n<blockquote>\n <p>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.<\/p>\n<\/blockquote>\n\n<p>I\u2019ve 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 <a href=\"https:\/\/www.drupal.org\/docs\/8\/umami-drupal-8-demonstration-installation-profile\">Unami demonstration profile<\/a> is pretty cool!<\/p>\n\n<p>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.<\/p>\n\n<p>I\u2019m already looking forward to see what\u2019s coming in 8.6 later this year!<\/p>\n\n<p>For more information on the 8.5 release, see the <a href=\"https:\/\/www.drupal.org\/blog\/drupal-8-5-0\">blog post on Drupal.org<\/a>.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-core"
|
||
]
|
||
}, {
|
||
"title": "Tweets from DrupalCamp London",
|
||
"path": "/articles/tweets-drupalcamp-london",
|
||
"is_draft": "false",
|
||
"created": "1520121600",
|
||
"excerpt": "I wasn\u2019t able to make it to DrupalCamp London, but here are some of the tweets that I saw.",
|
||
"body": "<p>In the end, I wasn\u2019t able to make it to DrupalCamp London because of the heavy snow that\u2019s hit the UK over the last few days. I did though keep a close eye on Twitter and still had good conversations with some of the attendees, so it did feel that in some ways I was still part of the conference.<\/p>\n\n<p>Thanks to <a href=\"https:\/\/twitter.com\/ChandeepKhosa\">@ChandeepKhosa<\/a>, <a href=\"https:\/\/twitter.com\/OrangePunchUK\">@OrangePunchUK<\/a>, <a href=\"https:\/\/twitter.com\/hussainweb\">@hussainweb<\/a>, <a href=\"https:\/\/twitter.com\/littlepixiez\">@littlepixiez<\/a>, <a href=\"https:\/\/twitter.com\/cferthorney\">@cferthorney<\/a> and others for taking the time to tweet whilst enjoying the event.<\/p>\n\n<p>Here are some of my favourites that I saw, and no snow next year, please!<\/p>\n\n<div class=\"flex flex-wrap -mx-4\">\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">View from outside <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> <a href=\"https:\/\/t.co\/b8wGe40Gem\">pic.twitter.com\/b8wGe40Gem<\/a><\/p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/969233707210104832?ref_src=twsrc%5Etfw\">March 1, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">This weekend at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> will be 5 years since my 1st <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> conference. It was life-changing, leading to travel across Europe learning, being inspired & speaking at 25 others. Thanks everyone who helped shape my journey, see you soon! <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/BeulE0XqET\">pic.twitter.com\/BeulE0XqET<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/969484539914604544?ref_src=twsrc%5Etfw\">March 2, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Programmes and badges getting unpacked <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> <a href=\"https:\/\/t.co\/v84c1bgCHu\">pic.twitter.com\/v84c1bgCHu<\/a><\/p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/969238918041391105?ref_src=twsrc%5Etfw\">March 1, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">.<a href=\"https:\/\/twitter.com\/MeganSanicki?ref_src=twsrc%5Etfw\">@MeganSanicki<\/a> sharing the future plans of what the <a href=\"https:\/\/twitter.com\/drupalassoc?ref_src=twsrc%5Etfw\">@drupalassoc<\/a> is working on next at <a href=\"https:\/\/twitter.com\/hashtag\/DCLCXO?src=hash&ref_src=twsrc%5Etfw\">#DCLCXO<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/XKw7zNgItz\">pic.twitter.com\/XKw7zNgItz<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/969529122937626625?ref_src=twsrc%5Etfw\">March 2, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">And it begins. <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> kicks off. <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/XoNkdRmMpx\">pic.twitter.com\/XoNkdRmMpx<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/969870984563085312?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">The unofficial group photo of <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/fntdWLfgCE\">pic.twitter.com\/fntdWLfgCE<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/969884647919407107?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Invoking the spirit of <a href=\"https:\/\/twitter.com\/markconroy?ref_src=twsrc%5Etfw\">@markconroy<\/a> <a href=\"https:\/\/twitter.com\/gareth5mm?ref_src=twsrc%5Etfw\">@gareth5mm<\/a> and <a href=\"https:\/\/twitter.com\/keithjay?ref_src=twsrc%5Etfw\">@keithjay<\/a> before kicking of the Umami Demo at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/t.co\/Yd8UqNUEuk\">pic.twitter.com\/Yd8UqNUEuk<\/a><\/p>— eli_t (@eli_t) <a href=\"https:\/\/twitter.com\/eli_t\/status\/969906274996572160?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Special mention of <a href=\"https:\/\/twitter.com\/navneet0693?ref_src=twsrc%5Etfw\">@navneet0693<\/a> by <a href=\"https:\/\/twitter.com\/eli_t?ref_src=twsrc%5Etfw\">@eli_t<\/a> for his contributions to Umami and winning a scholarship to attend <a href=\"https:\/\/twitter.com\/DrupalConNA?ref_src=twsrc%5Etfw\">@DrupalConNA<\/a>. Contributing helps everyone and we should encourage that. <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/LJIxLC10Hx\">pic.twitter.com\/LJIxLC10Hx<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/969917473737801729?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Listening to <a href=\"https:\/\/twitter.com\/michelvanvelde?ref_src=twsrc%5Etfw\">@michelvanvelde<\/a> session about the 5 benefits to get organised as a local Drupal community <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/r1k5yurZLu\">pic.twitter.com\/r1k5yurZLu<\/a><\/p>— Baddy Breidert (@baddysonja) <a href=\"https:\/\/twitter.com\/baddysonja\/status\/969941266866868224?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">"Reporting and Analytics in Drupal Commerce 2.x" with <a href=\"https:\/\/twitter.com\/ryanszrama?ref_src=twsrc%5Etfw\">@ryanszrama<\/a>, exciting reporting modules for <a href=\"https:\/\/twitter.com\/drupalcommerce?ref_src=twsrc%5Etfw\">@drupalcommerce<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/t.co\/ouE1MfRAqC\">pic.twitter.com\/ouE1MfRAqC<\/a><\/p>— Tawny Bartlett (@littlepixiez) <a href=\"https:\/\/twitter.com\/littlepixiez\/status\/969915175993257984?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Here we go... <a href=\"https:\/\/t.co\/NnoEw1oSYH\">pic.twitter.com\/NnoEw1oSYH<\/a><\/p>— David Thorne (@cferthorney) <a href=\"https:\/\/twitter.com\/cferthorney\/status\/969968283188424704?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">"Listen up Drupal!" with slootjes from <a href=\"https:\/\/twitter.com\/MediaMonks?ref_src=twsrc%5Etfw\">@MediaMonks<\/a>, learning about and utilising the powerful <a href=\"https:\/\/twitter.com\/hashtag\/Symfony?src=hash&ref_src=twsrc%5Etfw\">#Symfony<\/a> components inside and outside of <a href=\"https:\/\/twitter.com\/hashtag\/Drupal8?src=hash&ref_src=twsrc%5Etfw\">#Drupal8<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/t.co\/OuG5xT6tnf\">pic.twitter.com\/OuG5xT6tnf<\/a><\/p>— Tawny Bartlett (@littlepixiez) <a href=\"https:\/\/twitter.com\/littlepixiez\/status\/969962087404457985?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Huge thanks to <a href=\"https:\/\/twitter.com\/ThunderCoreTeam?ref_src=twsrc%5Etfw\">@ThunderCoreTeam<\/a> for sponsoring the social night. We went through \u00a31000 in 40mins! <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/lT86MBj6j3\">pic.twitter.com\/lT86MBj6j3<\/a><\/p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/969994553171300352?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">This <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> keynote by <a href=\"https:\/\/twitter.com\/ryanszrama?ref_src=twsrc%5Etfw\">@ryanszrama<\/a> is going to be required watching for anyone building\/running a OSS based business.<br><br>\u201cWhat good is it for someone to gain the world but forfeit their soul in the process?\u201d<\/p>— Chris Teitzel (@technerdteitzel) <a href=\"https:\/\/twitter.com\/technerdteitzel\/status\/969879969009631232?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">"Why would anybody not be using [Warden]?" - awesome feedback from the audience at Mike Davis's <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> presentation. <a href=\"https:\/\/twitter.com\/DeesonAgency?ref_src=twsrc%5Etfw\">@DeesonAgency<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupalcamplondon?src=hash&ref_src=twsrc%5Etfw\">#drupalcamplondon<\/a> <a href=\"https:\/\/t.co\/qZO50GucpO\">pic.twitter.com\/qZO50GucpO<\/a><\/p>— James Ford (@psyked) <a href=\"https:\/\/twitter.com\/psyked\/status\/969961851768500224?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:pb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Well this was ironic given I'm talking about <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> 8 <a href=\"https:\/\/twitter.com\/hashtag\/migrate?src=hash&ref_src=twsrc%5Etfw\">#migrate<\/a> today at 1205 B200 <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> cc <a href=\"https:\/\/twitter.com\/softescu?ref_src=twsrc%5Etfw\">@softescu<\/a> <a href=\"https:\/\/t.co\/CRVOp9bHVs\">pic.twitter.com\/CRVOp9bHVs<\/a><\/p>— eli_t (@eli_t) <a href=\"https:\/\/twitter.com\/eli_t\/status\/970214376539074560?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><\/3><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Final day <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> <a href=\"https:\/\/t.co\/gTbj8VoQw0\">pic.twitter.com\/gTbj8VoQw0<\/a><\/p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/970213706087960577?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><\/3><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">My first post on d.o was a comment <a href=\"https:\/\/t.co\/k1QSl8Rh81\">https:\/\/t.co\/k1QSl8Rh81<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupalcamp?src=hash&ref_src=twsrc%5Etfw\">#drupalcamp<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupalorigins?src=hash&ref_src=twsrc%5Etfw\">#drupalorigins<\/a><\/p>— Richard Sheppard (@siliconmeadow) <a href=\"https:\/\/twitter.com\/siliconmeadow\/status\/970242584793710592?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><\/3><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Meet my <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> family in <a href=\"https:\/\/twitter.com\/hashtag\/Manchester?src=hash&ref_src=twsrc%5Etfw\">#Manchester<\/a> <a href=\"https:\/\/twitter.com\/nwdug?ref_src=twsrc%5Etfw\">@nwdug<\/a> <a href=\"https:\/\/twitter.com\/eli_t?ref_src=twsrc%5Etfw\">@eli_t<\/a> <a href=\"https:\/\/twitter.com\/iriinamacovei?ref_src=twsrc%5Etfw\">@iriinamacovei<\/a> <a href=\"https:\/\/t.co\/3dVTpr4FvZ\">pic.twitter.com\/3dVTpr4FvZ<\/a><\/p>— Rakesh James (@RAKESH_JAMES) <a href=\"https:\/\/twitter.com\/RAKESH_JAMES\/status\/970250565807759360?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Big thanks to the <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> team. It's been a great conference! <a href=\"https:\/\/t.co\/lq7r9QVSYd\">pic.twitter.com\/lq7r9QVSYd<\/a><\/p>— William Mortada (@wmortada) <a href=\"https:\/\/twitter.com\/wmortada\/status\/970242269130444800?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">One of the best things at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> is how far away international friends come from to speak here. It\u2019s always a pleasure meeting <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> from Germany & <a href=\"https:\/\/twitter.com\/hussainweb?ref_src=twsrc%5Etfw\">@hussainweb<\/a> from India <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/bNj8e8ZOY2\">pic.twitter.com\/bNj8e8ZOY2<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970281087539785728?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Was feeling tired but instantly reinspired with a wonderful keynote by <a href=\"https:\/\/twitter.com\/technerdteitzel?ref_src=twsrc%5Etfw\">@technerdteitzel<\/a>... We are all superheroes! <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupalcommunity?src=hash&ref_src=twsrc%5Etfw\">#drupalcommunity<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> \ud83d\udca7 <a href=\"https:\/\/t.co\/49j7DCJUpH\">pic.twitter.com\/49j7DCJUpH<\/a><\/p>— Tawny Bartlett (@littlepixiez) <a href=\"https:\/\/twitter.com\/littlepixiez\/status\/970242734576586752?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Listening to <a href=\"https:\/\/twitter.com\/technerdteitzel?ref_src=twsrc%5Etfw\">@technerdteitzel<\/a> remind us that what we\u2019re able to do with <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> is magic to most people to kick off <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a>. \ud83e\uddd9\ud83c\udffc\u200d\u2640\ufe0f <a href=\"https:\/\/t.co\/826IdAQ31E\">pic.twitter.com\/826IdAQ31E<\/a><\/p>— Ryan Szrama (@ryanszrama) <a href=\"https:\/\/twitter.com\/ryanszrama\/status\/970238112818397184?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Hanging out with some of the most coolest people at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/zXFZ7VkPUc\">pic.twitter.com\/zXFZ7VkPUc<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970280364626366464?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\"><a href=\"https:\/\/twitter.com\/HornCologne?ref_src=twsrc%5Etfw\">@HornCologne<\/a> and <a href=\"https:\/\/twitter.com\/cyberswat?ref_src=twsrc%5Etfw\">@cyberswat<\/a> on radically reducing infrastructure-related drudgery at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/t.co\/eUsTuZI46z\">pic.twitter.com\/eUsTuZI46z<\/a><\/p>— Eugene Sia (@siaeugene) <a href=\"https:\/\/twitter.com\/siaeugene\/status\/969956287726456833?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Happy, smiling people from the UK, USA & Finland hanging out & sharing knowledge at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> : <a href=\"https:\/\/twitter.com\/GreenyBeans84?ref_src=twsrc%5Etfw\">@GreenyBeans84<\/a> <a href=\"https:\/\/twitter.com\/technerdteitzel?ref_src=twsrc%5Etfw\">@technerdteitzel<\/a> <a href=\"https:\/\/twitter.com\/KmgLaura?ref_src=twsrc%5Etfw\">@KmgLaura<\/a> <a href=\"https:\/\/twitter.com\/ryanszrama?ref_src=twsrc%5Etfw\">@ryanszrama<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/3o2iUCwoR0\">pic.twitter.com\/3o2iUCwoR0<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970305284194349056?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Awesome <a href=\"https:\/\/twitter.com\/hashtag\/migrate?src=hash&ref_src=twsrc%5Etfw\">#migrate<\/a> presentation <a href=\"https:\/\/twitter.com\/eli_t?ref_src=twsrc%5Etfw\">@eli_t<\/a> and awesome monkeys <a href=\"https:\/\/twitter.com\/thedeptofdesign?ref_src=twsrc%5Etfw\">@thedeptofdesign<\/a> \ud83d\udc35\ud83d\ude48\ud83d\ude49\ud83d\ude4a <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a><\/p>— Timo Kirkkala (@kirkkala) <a href=\"https:\/\/twitter.com\/kirkkala\/status\/970289205250265088?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Here are the slides from my session today <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> - flexible content editing with paragraphs in Drupal 8 <a href=\"https:\/\/t.co\/ozalJ06rL7\">https:\/\/t.co\/ozalJ06rL7<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a><\/p>— Baddy Breidert (@baddysonja) <a href=\"https:\/\/twitter.com\/baddysonja\/status\/970048010100101121?ref_src=twsrc%5Etfw\">March 3, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Slides from yesterday's presentation "Learning Frontend as a Backender" at <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/t.co\/IltzwIEcOF\">https:\/\/t.co\/IltzwIEcOF<\/a><\/p>— David Thorne Limited (@davidthorneltd) <a href=\"https:\/\/twitter.com\/davidthorneltd\/status\/970215719072927744?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Sharing the awesome story of how <a href=\"https:\/\/twitter.com\/DrupalEurope?ref_src=twsrc%5Etfw\">@DrupalEurope<\/a> has come about and will take place in September <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/yJj5i6tMxV\">pic.twitter.com\/yJj5i6tMxV<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970301979611353089?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">"Enable mentors to get credits." <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/QsWA61xpid\">pic.twitter.com\/QsWA61xpid<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/970308301408915457?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">A bit of snow brings the UK to a halt but it doesn't stop Iceland! Closing keynote from <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/robotsoccerchampion?src=hash&ref_src=twsrc%5Etfw\">#robotsoccerchampion<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/t.co\/f7o4XA0rHo\">pic.twitter.com\/f7o4XA0rHo<\/a><\/p>— Orange Punch (@OrangePunchUK) <a href=\"https:\/\/twitter.com\/OrangePunchUK\/status\/970303378843684869?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Three steps to improve:<br>1. Improve training quality.<br>2. Create facilities for _everyone_.<br>3. Start early (Train children).<a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/mJK3NctzG8\">pic.twitter.com\/mJK3NctzG8<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/970307570865987584?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">There were 50+ <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> events in 2017, just in Europe. Creating a template is extremely important to sustain the effort. It doesn't help anyone is every camp organiser ends up burnt out. <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/Diq1aid36a\">pic.twitter.com\/Diq1aid36a<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/970309217965010944?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">.<a href=\"https:\/\/twitter.com\/railsgirls?ref_src=twsrc%5Etfw\">@railsgirls<\/a> have an awesome set of resources & templates that help women to very easily create new local events around the world. We can learn a lot from them in the <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> community- <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/jxT29YGAvy\">pic.twitter.com\/jxT29YGAvy<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970310975483269120?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Thank you all dear <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> organizers and <a href=\"https:\/\/twitter.com\/hashtag\/keynote?src=hash&ref_src=twsrc%5Etfw\">#keynote<\/a> speakers... Amazing work.. great job <a href=\"https:\/\/twitter.com\/hashtag\/drupal?src=hash&ref_src=twsrc%5Etfw\">#drupal<\/a> <a href=\"https:\/\/twitter.com\/ryanszrama?ref_src=twsrc%5Etfw\">@ryanszrama<\/a> <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> <a href=\"https:\/\/twitter.com\/aburrows?ref_src=twsrc%5Etfw\">@aburrows<\/a> <a href=\"https:\/\/t.co\/FiS2I5lBjs\">pic.twitter.com\/FiS2I5lBjs<\/a><\/p>— Rakesh James (@RAKESH_JAMES) <a href=\"https:\/\/twitter.com\/RAKESH_JAMES\/status\/970318834581360641?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">A great weekend. Thanks to the organisers for putting on a great show. Thanks to <a href=\"https:\/\/twitter.com\/ReasonDigital?ref_src=twsrc%5Etfw\">@ReasonDigital<\/a> for sending me. Lots of new stuff to try out on Monday! <a href=\"https:\/\/t.co\/Xxvl7ZYcqj\">https:\/\/t.co\/Xxvl7ZYcqj<\/a><\/p>— Tom Metcalfe (@blwd_uk) <a href=\"https:\/\/twitter.com\/blwd_uk\/status\/970322074819530752?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">It\u2019s really amazing news that <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> camp organisers will now finally be credited on their <a href=\"https:\/\/t.co\/PO2gpSItnz\">https:\/\/t.co\/PO2gpSItnz<\/a> profiles - <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a> at <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/dHI5X7zBHk\">pic.twitter.com\/dHI5X7zBHk<\/a><\/p>— Chandeep Khosa (@ChandeepKhosa) <a href=\"https:\/\/twitter.com\/ChandeepKhosa\/status\/970311878202322945?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Discovering how to see the bigger picture! <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> ends up with this amazing keynote delivered by <a href=\"https:\/\/twitter.com\/baddysonja?ref_src=twsrc%5Etfw\">@baddysonja<\/a>. <a href=\"https:\/\/twitter.com\/hashtag\/dclondon?src=hash&ref_src=twsrc%5Etfw\">#dclondon<\/a> <a href=\"https:\/\/t.co\/bo0vgAvwxI\">pic.twitter.com\/bo0vgAvwxI<\/a><\/p>— Laura Ionescu (@Laura_MStrategy) <a href=\"https:\/\/twitter.com\/Laura_MStrategy\/status\/970311251325800448?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Thank you everyone who helped organise <a href=\"https:\/\/twitter.com\/DrupalCampLDN?ref_src=twsrc%5Etfw\">@DrupalCampLDN<\/a> <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> <a href=\"https:\/\/t.co\/hRtl7950CW\">pic.twitter.com\/hRtl7950CW<\/a><\/p>— hussainweb (@hussainweb) <a href=\"https:\/\/twitter.com\/hussainweb\/status\/970299951329828865?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">Great <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a> event again, so rewarding to see everyone there. Until next year! <a href=\"https:\/\/twitter.com\/hashtag\/Drupal?src=hash&ref_src=twsrc%5Etfw\">#Drupal<\/a> @\u2026 <a href=\"https:\/\/t.co\/DXCEaVzi9j\">https:\/\/t.co\/DXCEaVzi9j<\/a><\/p>— Paul (@Paul_Rowell) <a href=\"https:\/\/twitter.com\/Paul_Rowell\/status\/970327065080758277?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n <div class=\"w-full md:w-1\/2 lg:w-1\/3 px-4 lg:mb-4\"><blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"en\" dir=\"ltr\">A huge huge huge thank you to all our sponsors, speakers, volunteers, attendees and everyone else involved in that Camp. It was a fantastic weekend! <br>Look out for the post Camp Survey and more! <a href=\"https:\/\/twitter.com\/hashtag\/DCLondon?src=hash&ref_src=twsrc%5Etfw\">#DCLondon<\/a><\/p>— DrupalCamp London (@DrupalCampLDN) <a href=\"https:\/\/twitter.com\/DrupalCampLDN\/status\/970319584904523777?ref_src=twsrc%5Etfw\">March 4, 2018<\/a><\/blockquote><\/div>\n<\/div>\n",
|
||
"tags": ["drupal"
|
||
,"drupalcamp"
|
||
,"drupalcamp-london"
|
||
]
|
||
}, {
|
||
"title": "Yay, the Mediacurrent Contrib Half Hour is Back!",
|
||
"path": "/articles/mediacurrent-contrib-half-hour-is-back",
|
||
"is_draft": "false",
|
||
"created": "1519948800",
|
||
"excerpt": "Mediacurrent\u2019s \"contrib half hour sessions\" are back.",
|
||
"body": "<p>Back in November, <a href=\"https:\/\/www.mediacurrent.com\/blog\/introducing-mediacurrent-contrib-half-hour\">Mediacurrent introduced<\/a> 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.<\/p>\n\n<p>From Damien's <a href=\"https:\/\/www.mediacurrent.com\/blog\/introducing-mediacurrent-contrib-half-hour\">introductory blog post<\/a>:<\/p>\n\n<blockquote>\n <p>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.<\/p>\n \n <p>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.<\/p>\n<\/blockquote>\n\n<p>There is also an <a href=\"https:\/\/www.mediacurrent.com\/blog\/updates-mediacurrent-contrib-half-hour-weekly-meeting\">updates blog post<\/a> that continues to show the latest information, and the video recordings are <a href=\"https:\/\/www.youtube.com\/playlist?list=PLu-MxhbnjI9rHroPvZO5LEUhr58Yl0j_F\">uploaded to YouTube<\/a> after the session. Here is the first one from November:<\/p>\n\n<!-- <div class=\"talk-video mb-4\">\n<iframe width=\"678\" height=\"408\" src=\"\/\/www.youtube.com\/embed\/8xHE5y1rA1g\" frameborder=\"0\" allowfullscreen><\/iframe>\n<\/div> -->\n\n<p>I enjoyed watching the first few videos, as I\u2019m 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\u2019t faded away.<\/p>\n\n<p>I\u2019m glad to see today that it\u2019s back and that all of the previous videos have been uploaded and added to the <a href=\"https:\/\/www.youtube.com\/playlist?list=PLu-MxhbnjI9rHroPvZO5LEUhr58Yl0j_F\">YouTube playlist<\/a>, and that <a href=\"https:\/\/www.mediacurrent.com\/blog\/updates-mediacurrent-contrib-half-hour-weekly-meeting\">on the update post<\/a> there are scheduled topics for the rest of this month including documentation and automated testing.<\/p>\n\n<div class=\"mb-4\">\n<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>\n<\/div>\n\n<p>I do enjoy watching these, and I like both the presentation and Q&A format that they alternate between. I\u2019ll look forward to catching up over the next few days, and to hopefully seeing them continue to be uploaded after future meetings.<\/p>\n\n<p>Thanks Damien and Mediacurrent!<\/p>\n",
|
||
"tags": ["contribution"
|
||
,"drupal"
|
||
,"open-source"
|
||
]
|
||
}, {
|
||
"title": "Building the new PHPSW Website",
|
||
"path": "/articles/building-the-new-phpsw-website",
|
||
"is_draft": "false",
|
||
"created": "1519776000",
|
||
"excerpt": "Earlier this week we had another hack night, working on the new PHPSW user group website.",
|
||
"body": "<p>Earlier this week we had another hack night, working on the new <a href=\"https:\/\/phpsw.uk\">PHPSW user group<\/a> website.<\/p>\n\n<div class=\"mb-4\">\n <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>\n<\/div>\n\n<p>It\u2019s built with Symfony so it\u2019s naturally using Twig for templating. I\u2019ve become a big fan of the utility based approach to CSS and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a> in particular, so I\u2019m using that for all of the styling, and using <a href=\"https:\/\/github.com\/symfony\/webpack-encore\">Webpack Encore<\/a> to compile all of the assets.<\/p>\n\n<p>We have an integration with Meetup.com which we\u2019re 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.<\/p>\n\n<p>We\u2019re 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 <a href=\"https:\/\/github.com\/phpsw\/phpsw-ng\">GitHub repository<\/a>.<\/p>\n",
|
||
"tags": ["phpsw"
|
||
,"symfony"
|
||
,"tailwind-css"
|
||
]
|
||
}, {
|
||
"title": "Queuing Private Messages in Drupal 8",
|
||
"path": "/articles/queuing-private-messages-in-drupal-8",
|
||
"is_draft": "false",
|
||
"created": "1519689600",
|
||
"excerpt": "Introducing the Private Message Queue module for Drupal 8.",
|
||
"body": "<p>My current project at <a href=\"https:\/\/microserve.io\">Microserve<\/a> is a Drupal 8 website that uses the <a href=\"https:\/\/www.drupal.org\/project\/private_message\">Private Message<\/a> module for users to send messages to each other.<\/p>\n\n<p>In some cases though, the threads could contain hundreds of recipients so I decided that it would be good to queue the message requests so that they can be processed as part of a background process for better performance. The Private Message module does not include this, so I've written and released a separate <a href=\"https:\/\/www.drupal.org\/project\/private_message_queue\">Private Message Queue<\/a> module.<\/p>\n\n<h2 id=\"queuing-a-message\">Queuing a Message<\/h2>\n\n<p>The module provices a <code>PrivateMessageQueuer<\/code> service (<code>private_message_queue.queuer<\/code>) which queues the items via the <code>queue()<\/code> method.<\/p>\n\n<p>The method accepts an array of <code>User<\/code> objects as the messsage recipients, the message body text and another user as the message owner. (I\u2019m currently considering <a href=\"https:\/\/www.drupal.org\/project\/private_message_queue\/issues\/2948233\">whether to make the owner optional<\/a>, and default to the current user if one is not specified)<\/p>\n\n<p>Here is an example:<\/p>\n\n<pre><code class=\"php\">$recipients = $this->getRecipients(); \/\/ An array of User objects.\n$message = 'Some message text';\n$owner = \\Drupal::currentUser();\n\n$queuer = \\Drupal::service('private_message_queue.queuer');\n$queuer->queue($recipients, $message, $owner);\n<\/code><\/pre>\n\n<p>These three pieces of data are then saved as part of the queued item. You can see these by checking the \"queue\" table in the database or by running <code>drush queue-list<\/code>.<\/p>\n\n<p><img src=\"\/images\/blog\/private-message-queue.png\" alt=\"\" \/><\/p>\n\n<pre><code>$ drush queue-list\nQueue Items Class\nprivate_message_queue 19 Drupal\\Core\\Queue\\DatabaseQueue\n<\/code><\/pre>\n\n<h2 id=\"processing-the-queue\">Processing the Queue<\/h2>\n\n<p>The module also provides a <code>PrivateMessageQueue<\/code> queue worker, which processes the queued items. For each item, it creates a new private message setting the owner and the message body.<\/p>\n\n<p>It uses the <code>PrivateMessageThread<\/code> class from the Private Message module to find for an existing thread for the specified recipients, or creates a new thread if one isn't found. The new message is then added to the thread.<\/p>\n\n<p>The queue is processed on each cron run, so I recommend adding a module like <a href=\"https:\/\/www.drupal.org\/project\/ultimate_cron\">Ultimate Cron<\/a> so that you can process the queued items frequently (e.g. every 15 minutes) and run the heavier tasks like checking for updates etc less frequently (e.g. once a day).<\/p>\n\n<p>You can also process the queue manually with Drush using the <code>drush queue-run <queue-name><\/code> command - e.g. <code>drush queue-run private_message_queue<\/code>.<\/p>\n\n<pre><code>$ drush queue-run private_message_queue\nProcessed 19 items from the private_message_queue queue in 3.34 sec.\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-8"
|
||
,"drupal-modules"
|
||
,"drupal-planet"
|
||
,"open-source"
|
||
]
|
||
}, {
|
||
"title": "Looking forward to DrupalCamp London",
|
||
"path": "/articles/looking-forward-to-drupalcamp-london",
|
||
"is_draft": "false",
|
||
"created": "1519689600",
|
||
"excerpt": "This weekend is DrupalCamp London 2018. I\u2019ll be there along with a number of my Microserve colleagues.",
|
||
"body": "<p>This weekend is <a href=\"https:\/\/drupalcamp.london\">DrupalCamp London 2018<\/a>. I\u2019ll be there along with a number of my <a href=\"https:\/\/microserve.io\">Microserve<\/a> colleagues.<\/p>\n\n<p>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 <a href=\"\/talks\/git-flow\">gave a talk<\/a> at, when I presented a session about Git Flow having given only one user group talk before.<\/p>\n\n<p>I\u2019ve presented sessions at every DrupalCamp London since (including two last year), and I\u2019m lucky enough to be <a href=\"\/talks\/deploying-drupal-fabric\">speaking again this year<\/a> due to one of the originally announced speakers no longer being able to make it to the event.<\/p>\n\n<p>Here are some other sessions that I\u2019m hoping to see (in no particular order):<\/p>\n\n<ul>\n<li>Keynote by <a href=\"http:\/\/ryanszrama.com\">Ryan Szrama<\/a> from <a href=\"https:\/\/commerceguys.com\">Commerce Guys<\/a><\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/drupal-8-services-and-dependency-injection\">Drupal 8 Services And Dependency Injection<\/a> by Phil Norton<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/growing-developers-drupal\">Growing developers with Drupal<\/a> by Fran Garcia-Linares (fjgarlin)<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/how-make-it-easier-newcomers-get-involved-drupal\">How to make it easier for newcomers to get involved in Drupal<\/a> by heather<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/lets-take-best-route-exploring-drupal-8-routing-system\">Let\u2019s take the best route - Exploring Drupal 8 Routing System<\/a> by surbhi<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/new-recipe-decoupling-drupal-8-symfony-and-slim-framework\">New recipe of Decoupling: Drupal 8, Symfony and Slim Framework<\/a> by Jyoti Singh<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/plugin-api-examples\">Plugin API by examples<\/a> by Gabriele (gambry)<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/value-mentorship-community\">Value of mentorship in the community<\/a> by Hussain Abbas (hussainweb)<\/li>\n<li><a href=\"https:\/\/drupalcamp.london\/session\/warden-helping-drupal-agencies-sleep-night\">Warden - Helping Drupal Agencies Sleep at Night<\/a> by Mike Davis<\/li>\n<\/ul>\n\n<p>Unfortunately there are some time slots where I\u2019d like to see more than one of the talks (including when I\u2019m going to be speaking). This regularly happens at conferences, but I\u2019ll look forward to watching those on <a href=\"https:\/\/www.youtube.com\/channel\/UCsaB96zszIP4Y3czs-ndiIA\">YouTube<\/a> after the event.<\/p>\n\n<p>I\u2019m also looking forward to catching up with former colleagues, spending some time in the \"hallway track\" and hopefully doing some sprinting too!<\/p>\n\n<h2 id=\"finally\">Finally<\/h2>\n\n<p>For nostalgia, <a href=\"\/blog\/2014\/02\/09\/drupalcamp-london-2014\">here\u2019s the blog post<\/a> that I wrote before I attended my first DrupalCamp London.<\/p>\n\n<p>See everyone this weekend!<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupalcamp"
|
||
,"drupalcamp-london"
|
||
,"speaking"
|
||
]
|
||
}, {
|
||
"title": "Using Tailwind CSS in your Drupal Theme",
|
||
"path": "/articles/using-tailwind-css-in-your-drupal-theme",
|
||
"is_draft": "false",
|
||
"created": "1517788800",
|
||
"excerpt": "What is Tailwind CSS, and how do I use it in Drupal theme?",
|
||
"body": "<h2 id=\"what-is-tailwind%3F\">What is Tailwind?<\/h2>\n\n<blockquote>\n <p>Tailwind is a utility-first CSS framework for rapidly building custom user interfaces.<\/p>\n<\/blockquote>\n\n<p>It generates a number of utility classes that you can add to your theme's markup to apply different styling, as well as the ability to apply classes to other markup and create components comprised of utility classes using a custom <code>@apply<\/code> PostCSS directive.<\/p>\n\n<h2 id=\"initial-configuration\">Initial Configuration<\/h2>\n\n<p>The installation and configuration steps are essentially the same as those outlined within the <a href=\"https:\/\/tailwindcss.com\/docs\/installation\">Tailwind documentation<\/a>, and should be performed within your custom theme's directory (e.g. <code>sites\/all\/themes\/custom\/mytheme<\/code> for Drupal 7 or <code>themes\/custom\/mytheme<\/code> for Drupal 8:<\/p>\n\n<ol>\n<li>Require PostCSS and Tailwind via <code>npm<\/code> or <code>yarn<\/code>.<\/li>\n<li>Generate a configuration file using <code>.\/node_modules\/.bin\/tailwind init<\/code>.<\/li>\n<li>Tweak the settings as needed.<\/li>\n<li>Add a <code>postcss.config.js<\/code> file.<\/li>\n<li>Configure your build tool (Gulp, Grunt, Webpack).<\/li>\n<li>Generate the CSS.<\/li>\n<li>Include a path to the generated CSS in your <code>MYTHEME.info<\/code>, <code>MYTHEME.info.yml<\/code> or <code>MYTHEME.libraries.yml<\/code> file.<\/li>\n<\/ol>\n\n<h2 id=\"postcss-configuration\">PostCSS Configuration<\/h2>\n\n<p>Create a <code>postcss.config.js<\/code> file and add <code>tailwindcss<\/code> as a plugin, passing the path to the config file:<\/p>\n\n<pre><code class=\"language-js\">module.exports = {\n plugins: [\n require('tailwindcss')('.\/tailwind.js'),\n ]\n}\n<\/code><\/pre>\n\n<h2 id=\"configuration-for-drupal\">Configuration for Drupal<\/h2>\n\n<p>There are some configuration settings within <code>tailwind.js<\/code> that you\u2019ll need to change to make things work nicely with Drupal. These are within the <code>options<\/code> section:<\/p>\n\n<pre><code class=\"language-js\">options: {\n prefix: 'tw-',\n important: true,\n ...\n}\n<\/code><\/pre>\n\n<h3 id=\"prefix\">Prefix<\/h3>\n\n<p>By adding a prefix like <code>tw-<\/code>, we can ensure that the Tailwind classes don\u2019t conflict with core HTML classes like <code>block<\/code>. We can also ensure that they won't conflict with any other existing HTML or CSS.<\/p>\n\n<p>No prefix:<\/p>\n\n<p><img src=\"\/images\/blog\/using-tailwind-drupal\/prefix-1.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>With prefix:<\/p>\n\n<p><img src=\"\/images\/blog\/using-tailwind-drupal\/prefix-2.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h3 id=\"important\">Important<\/h3>\n\n<p>We can also set the <code>!important<\/code> rule on all Tailwind\u2019s generated classes. We need to do this if we want to override core styles which have more specific rules.<\/p>\n\n<p>For example: if I had this core markup then the left margin added by <code>tw-ml-4<\/code> would be overridden by core\u2019s <code>.item-list ul<\/code> styling.<\/p>\n\n<pre><code class=\"language-html\"><div class=\"item-list\">\n <ul class=\"tw-ml-4\">\n ...\n <\/ul>\n<\/div>\n<\/code><\/pre>\n\n<p><img src=\"\/images\/blog\/using-tailwind-drupal\/important-1.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>With the <code>!important<\/code> rule enabled though, the Tailwind\u2019s class takes precedence and is applied.<\/p>\n\n<p><img src=\"\/images\/blog\/using-tailwind-drupal\/important-2.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h2 id=\"example\">Example<\/h2>\n\n<p>For an example of Tailwind within a Drupal 8 theme, see the custom theme for the <a href=\"https:\/\/github.com\/drupalbristol\/drupal-bristol-website\/tree\/master\/web\/themes\/custom\/drupalbristol\">Drupal Bristol website<\/a> on GitHub.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drupal-theming"
|
||
,"tailwind-css"
|
||
]
|
||
}, {
|
||
"title": "DrupalCamp Bristol 2018 Statement",
|
||
"path": "/articles/drupalcamp-bristol-2018",
|
||
"is_draft": "false",
|
||
"created": "1517270400",
|
||
"excerpt": "Unfortunately, we won\u2019t be running DrupalCamp Bristol this year.",
|
||
"body": "<p>It\u2019s with heavy hearts that we are announcing there won\u2019t 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.<\/p>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<p>In our washup of last year\u2019s 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 \u2018Drupal\u2019 aspect of the camp, but the benefits it brings in terms of bringing together different views gives the camp greater value in our eyes.<\/p>\n\n<p>It\u2019s because of this mix of talks and wider shifts in the community in \u2018getting us off the island\u2019 that we have been thinking about rebranding to reflect the mix of talks that the camp hosts. The fact is DrupalCamps don\u2019t just cover Drupal anymore. There is Symfony, Composer, OOP principles, React, etc.<\/p>\n\n<p>We\u2019ll 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!<\/p>\n\n<p>The DrupalCamp Bristol organising committee<\/p>\n\n<p>Tom, Ollie, Emily, Sophie, Rob, Mark<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupalcamp-bristol"
|
||
]
|
||
}, {
|
||
"title": "Writing a new Drupal 8 Module using Test Driven Development (TDD)",
|
||
"path": "/articles/writing-drupal-module-test-driven-development-tdd",
|
||
"is_draft": "false",
|
||
"created": "1510012800",
|
||
"excerpt": "How to write automated tests and follow test driven development for Drupal modules.",
|
||
"body": "<p class=\"text-center\"><img src=\"\/images\/blog\/drupalcamp-dublin.jpg\" alt=\"\" \/><\/p>\n\n<p>I recently gave a <a href=\"\/talks\/tdd-test-driven-drupal\">talk on automated testing in Drupal<\/a> talk at <a href=\"http:\/\/2017.drupal.ie\">DrupalCamp Dublin<\/a> and as a lunch and learn session for my colleagues at Microserve. As part of the talk, I gave an example of how to build a Drupal 8 module using a test driven approach. I\u2019ve released the <a href=\"https:\/\/github.com\/opdavies\/tdd_dublin\">module code on GitHub<\/a>, and this post outlines the steps of the process.<\/p>\n\n<h2 id=\"prerequisites\">Prerequisites<\/h2>\n\n<p>You have created a <code>core\/phpunit.xml<\/code> file based on <code>core\/phpunit.xml.dist<\/code>, and populated it with your database credentials so that PHPUnit can bootstrap the Drupal database as part of the tests. <a href=\"https:\/\/gist.github.com\/opdavies\/dc5f0cea46ccd349b34a9f3a463c14bb\">Here is an example<\/a>.<\/p>\n\n<h2 id=\"acceptance-criteria\">Acceptance Criteria<\/h2>\n\n<p>For the module, we are going to satisfy this example acceptance criteria:<\/p>\n\n<blockquote>\n <p>As a site visitor,<br>\n I want to see all published pages at \/pages<br>\n Ordered alphabetically by title<\/p>\n<\/blockquote>\n\n<h2 id=\"initial-setup\">Initial Setup<\/h2>\n\n<p>Let\u2019s start by writing the minimal code needed in order for the new module to\nbe enabled. In Drupal 8, this is the <code>.info.yml<\/code> file.<\/p>\n\n<pre><code class=\"language-yaml\"># tdd_dublin.info.yml\n\nname: 'TDD Dublin'\nexcerpt: 'A demo module for DrupalCamp Dublin to show test driven module development.'\ncore: 8.x\ntype: module\n<\/code><\/pre>\n\n<p>We can also add the test file structure at this point too. We\u2019ll call it <code>PageTestTest.php<\/code> and put it within a <code>tests\/src\/Functional<\/code> directory. As this is a functional test, it extends the <code>BrowserTestBase<\/code> class, and we need to ensure that the tdd_dublin module is enabled by adding it to the <code>$modules<\/code> array.<\/p>\n\n<pre><code class=\"language-php\">\/\/ tests\/src\/Functional\/PageListTest.php\n\nnamespace Drupal\\Tests\\tdd_dublin\\Functional;\n\nuse Drupal\\Tests\\BrowserTestBase\\BrowserTestBase;\n\nclass PageListTest extends BrowserTestBase {\n\n protected static $modules = ['tdd_dublin'];\n\n}\n<\/code><\/pre>\n\n<p>With this in place, we can now start adding test methods.<\/p>\n\n<h2 id=\"ensure-that-the-listing-page-exists\">Ensure that the Listing page Exists<\/h2>\n\n<h3 id=\"writing-the-first-test\">Writing the First Test<\/h3>\n\n<p>Let\u2019s start by testing that the listing page exists at \/pages. We can do this by loading the page and checking the status code. If the page exists, the code will be 200, otherwise it will be 404.<\/p>\n\n<p>I usually like to write comments first within the test method, just to outline the steps that I'm going to take and then replace it with code.<\/p>\n\n<pre><code class=\"language-php\">public function testListingPageExists() {\n \/\/ Go to \/pages and check that it is accessible by checking the status\n \/\/ code.\n}\n<\/code><\/pre>\n\n<p>We can use the <code>drupalGet()<\/code> method to browse to the required path, i.e. <code>\/pages<\/code>, and then write an assertion for the response code value.<\/p>\n\n<pre><code class=\"language-php\">public function testListingPageExists() {\n $this->drupalGet('pages');\n\n $this->assertSession()->statusCodeEquals(200);\n}\n<\/code><\/pre>\n\n<h3 id=\"running-the-test\">Running the Test<\/h3>\n\n<p>In order to run the tests, you either need to include <code>-c core<\/code> or be inside the <code>core<\/code> directory when running the command, to ensure that the test classes are autoloaded so can be found, though the path to the <code>vendor<\/code> directory may be different depending on your project structure. You can also specify a path within which to run the tests - e.g. within the module\u2019s <code>test<\/code> directory.<\/p>\n\n<pre><code class=\"language-plain\">$ vendor\/bin\/phpunit -c core modules\/custom\/tdd_dublin\/tests\n<\/code><\/pre>\n\n<div class=\"note\">\n\n<p>Note: I\u2019m using Docksal, and I\u2019ve noticed that I need to run the tests from within the CLI container. You can do this by running the <code>fin bash<\/code> command.<\/p>\n\n<\/div>\n\n<pre><code class=\"language-plain\">1) Drupal\\Tests\\tdd_dublin\\Functional\\PageListTest::testListingPageExists\nBehat\\Mink\\Exception\\ExpectationException: Current response status code is 404, but 200 expected.\n\nFAILURES!\nTests: 1, Assertions: 1, Errors: 1.\n<\/code><\/pre>\n\n<p>Because the route does not yet exist, the response code returned is 404, so the test fails.<\/p>\n\n<p>Now we can make it pass by adding the page. For this, I will use the Views module, though you could achieve the same result with a custom route and a Controller.<\/p>\n\n<h3 id=\"building-the-view\">Building the View<\/h3>\n\n<p>To begin with, I will create a view showing all types of content with a default sort order of newest first. We will use further tests to ensure that only the correct content is returned and that it is ordered correctly.<\/p>\n\n<p><img src=\"\/images\/blog\/tdd-drupal-1.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>The only addition I will make to the view is to add a path at <code>pages<\/code>, as per the acceptance criteria.<\/p>\n\n<p><img src=\"\/images\/blog\/tdd-drupal-2.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<h3 id=\"exporting-the-view\">Exporting the View<\/h3>\n\n<p>With the first version of the view built, it needs to be incldued within the module so that it can be enabled when the test is run. To do this, we need to export the configuration for the view, and place it within the module\u2019s <code>config\/install<\/code> directory. This can be done using the <code>drush config-export<\/code> command or from within the Drupal UI. In either case, the <code>uid<\/code> line at the top of the file needs to be removed so the configuration can be installed.<\/p>\n\n<p>Here is the exported view configuration:<\/p>\n\n<pre><code class=\"language-yaml\">langcode: en\nstatus: true\ndependencies:\n module:\n - node\n - user\nid: pages\nlabel: pages\nmodule: views\nexcerpt: ''\ntag: ''\nbase_table: node_field_data\nbase_field: nid\ncore: 8.x\ndisplay:\n default:\n display_plugin: default\n id: default\n display_title: Master\n position: 0\n display_options:\n access:\n type: perm\n options:\n perm: 'access content'\n cache:\n type: tag\n options: { }\n query:\n type: views_query\n options:\n disable_sql_rewrite: false\n distinct: false\n replica: false\n query_comment: ''\n query_tags: { }\n exposed_form:\n type: basic\n options:\n submit_button: Apply\n reset_button: false\n reset_button_label: Reset\n exposed_sorts_label: 'Sort by'\n expose_sort_order: true\n sort_asc_label: Asc\n sort_desc_label: Desc\n pager:\n type: mini\n options:\n items_per_page: 10\n offset: 0\n id: 0\n total_pages: null\n expose:\n items_per_page: false\n items_per_page_label: 'Items per page'\n items_per_page_options: '5, 10, 25, 50'\n items_per_page_options_all: false\n items_per_page_options_all_label: '- All -'\n offset: false\n offset_label: Offset\n tags:\n previous: \u2039\u2039\n next: \u203a\u203a\n style:\n type: default\n options:\n grouping: { }\n row_class: ''\n default_row_class: true\n uses_fields: false\n row:\n type: fields\n options:\n inline: { }\n separator: ''\n hide_empty: false\n default_field_elements: true\n fields:\n title:\n id: title\n table: node_field_data\n field: title\n entity_type: node\n entity_field: title\n label: ''\n alter:\n alter_text: false\n make_link: false\n absolute: false\n trim: false\n word_boundary: false\n ellipsis: false\n strip_tags: false\n html: false\n hide_empty: false\n empty_zero: false\n settings:\n link_to_entity: true\n plugin_id: field\n relationship: none\n group_type: group\n admin_label: ''\n exclude: false\n element_type: ''\n element_class: ''\n element_label_type: ''\n element_label_class: ''\n element_label_colon: true\n element_wrapper_type: ''\n element_wrapper_class: ''\n element_default_classes: true\n empty: ''\n hide_alter_empty: true\n click_sort_column: value\n type: string\n group_column: value\n group_columns: { }\n group_rows: true\n delta_limit: 0\n delta_offset: 0\n delta_reversed: false\n delta_first_last: false\n multi_type: separator\n separator: ', '\n field_api_classes: false\n filters:\n status:\n value: '1'\n table: node_field_data\n field: status\n plugin_id: boolean\n entity_type: node\n entity_field: status\n id: status\n expose:\n operator: ''\n group: 1\n sorts:\n created:\n id: created\n table: node_field_data\n field: created\n order: DESC\n entity_type: node\n entity_field: created\n plugin_id: date\n relationship: none\n group_type: group\n admin_label: ''\n exposed: false\n expose:\n label: ''\n granularity: second\n header: { }\n footer: { }\n empty: { }\n relationships: { }\n arguments: { }\n display_extenders: { }\n cache_metadata:\n max-age: -1\n contexts:\n - 'languages:language_content'\n - 'languages:language_interface'\n - url.query_args\n - 'user.node_grants:view'\n - user.permissions\n tags: { }\n page_1:\n display_plugin: page\n id: page_1\n display_title: Page\n position: 1\n display_options:\n display_extenders: { }\n path: pages\n cache_metadata:\n max-age: -1\n contexts:\n - 'languages:language_content'\n - 'languages:language_interface'\n - url.query_args\n - 'user.node_grants:view'\n - user.permissions\n tags: { }\n<\/code><\/pre>\n\n<p>When the test is run again, we see a different error that leads us to the next step.<\/p>\n\n<pre><code class=\"language-plain\">1) Drupal\\Tests\\tdd_dublin\\Functional\\PageListTest::testListingPageExists\nDrupal\\Core\\Config\\UnmetDependenciesException: Configuration objects provided by <em class=\"placeholder\">tdd_dublin<\/em> have unmet dependencies: <em class=\"placeholder\">node.type.page (node), views.view.pages (node, views)<\/em>\n\nFAILURES!\nTests: 1, Assertions: 0, Errors: 1.\n<\/code><\/pre>\n\n<p>This error is identifying unmet dependencies within the module\u2019s configuration. In this case, the view that we\u2019ve added depends on the node and views modules, but these aren\u2019t enabled. To fix this, we can add the extra modules as dependencies of tdd_dublin so they will be enabled too.<\/p>\n\n<pre><code class=\"language-yaml\"># tdd_dublin.info.yml\n\ndependencies:\n - drupal:node\n - drupal:views\n<\/code><\/pre>\n\n<pre><code class=\"language-plain\">1) Drupal\\Tests\\tdd_dublin\\Functional\\PageListTest::testListingPageExists\nDrupal\\Core\\Config\\UnmetDependenciesException: Configuration objects provided by <em class=\"placeholder\">tdd_dublin<\/em> have unmet dependencies: <em class=\"placeholder\">views.view.pages (node.type.page)<\/em>\n\nFAILURES!\nTests: 1, Assertions: 0, Errors: 1.\n<\/code><\/pre>\n\n<p>With the modules enabled, we can see one more unmet dependency for <code>node.type.page<\/code>. This means that we need a page content type to be able to install the view. We can fix this in the same way as before, by exporting the configuration and copying it into the <code>config\/install<\/code> directory.<\/p>\n\n<p>With this in place, the test should now pass - and it does.<\/p>\n\n<pre><code class=\"language-plain\">Time: 26.04 seconds, Memory: 6.00MB\n\nOK (1 test, 1 assertion)\n<\/code><\/pre>\n\n<p>We now have a test to ensure that the listing page exists.<\/p>\n\n<h2 id=\"ensure-that-only-published-pages-are-shown\">Ensure that only Published Pages are Shown<\/h2>\n\n<h3 id=\"writing-the-test\">Writing the Test<\/h3>\n\n<p>Now that we have a working page, we can now move on to checking that the correct content is returned. Again, I\u2019ll start by writing comments and then translate that into code.<\/p>\n\n<p>The objectives of this test are:<\/p>\n\n<ul>\n<li>To ensure that only page nodes are returned.<\/li>\n<li>To ensure that only published nodes are returned.<\/li>\n<\/ul>\n\n<pre><code class=\"language-php\">public function testOnlyPublishedPagesAreShown() {\n \/\/ Given that a have a mixture of published and unpublished pages, as well\n \/\/ as other types of content.\n\n \/\/ When I view the page.\n\n \/\/ Then I should only see the published pages.\n}\n<\/code><\/pre>\n\n<p>In order to test the different scenarios, I will create an additional \"article\" content type, create a node of this type as well as one published and one unpublished page. From this combination, I only expect one node to be visible.<\/p>\n\n<pre><code class=\"language-php\">public function testOnlyPublishedPagesAreShown() {\n $this->drupalCreateContentType(['type' => 'article']);\n\n $this->drupalCreateNode(['type' => 'page', 'status' => TRUE]);\n $this->drupalCreateNode(['type' => 'article']);\n $this->drupalCreateNode(['type' => 'page', 'status' => FALSE]);\n\n \/\/ When I view the page.\n\n \/\/ Then I should only see the published pages.\n}\n<\/code><\/pre>\n\n<p>We could use <code>drupalGet()<\/code> again to browse to the page and write assertions based on the rendered HTML, though I\u2019d rather do this against the data returned from the view itself. This is so that the test isn\u2019t too tightly coupled to the presentation logic, and we won\u2019t be in a situation where at a later date the test fails because of changes made to how the data is displayed.<\/p>\n\n<p>Rather, I\u2019m going to use <code>views_get_view_result()<\/code> to programmatically get the result of the view. This returns an array of <code>Drupal\\views\\ResultRow<\/code> objects, which contain the nodes. I can use <code>array_column<\/code> to extract the node IDs from the view result into an array.<\/p>\n\n<pre><code class=\"language-php\">public function testOnlyPublishedPagesAreShown() {\n $this->drupalCreateContentType(['type' => 'article']);\n\n $this->drupalCreateNode(['type' => 'page', 'status' => TRUE]);\n $this->drupalCreateNode(['type' => 'article']);\n $this->drupalCreateNode(['type' => 'page', 'status' => FALSE]);\n\n $result = views_get_view_result('pages');\n $nids = array_column($result, 'nid');\n\n \/\/ Then I should only see the published pages.\n}\n<\/code><\/pre>\n\n<p>From the generated nodes, I can use <code>assertEquals()<\/code> to compare the returned node IDs from the view against an array of expected node IDs - in this case, I expect only node 1 to be returned.<\/p>\n\n<pre><code class=\"language-php\">public function testOnlyPublishedPagesAreShown() {\n $this->drupalCreateContentType(['type' => 'article']);\n\n $this->drupalCreateNode(['type' => 'page', 'status' => TRUE]);\n $this->drupalCreateNode(['type' => 'article']);\n $this->drupalCreateNode(['type' => 'page', 'status' => FALSE]);\n\n $result = views_get_view_result('pages');\n $nids = array_column($result, 'nid');\n\n $this->assertEquals([1], $nids);\n}\n<\/code><\/pre>\n\n<h3 id=\"running-the-test\">Running the Test<\/h3>\n\n<p>The test fails as no extra conditions have been added to the view, though the default \"Content: Published\" filter is already excluding one of the page nodes. We can see from the output from the test that node 1 (a page) and node 2 (the article) are both being returned.<\/p>\n\n<pre><code class=\"language-plain\">1) Drupal\\Tests\\tdd_dublin\\Functional\\PageListTest::testOnlyPublishedPagesAreShown\nFailed asserting that two arrays are equal.\n--- Expected\n+++ Actual\n@@ @@\n Array (\n- 0 => 1\n+ 0 => '2'\n+ 1 => '1'\n )\n\nFAILURES!\nTests: 1, Assertions: 3, Failures: 1.\n<\/code><\/pre>\n\n<h3 id=\"updating-the-test\">Updating the Test<\/h3>\n\n<p>We can fix this by adding another condition to the view, to only show content based on the node type - i.e. only return page nodes.<\/p>\n\n<p><img src=\"\/images\/blog\/tdd-drupal-3.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>Once the view is updated and the configuration is updated within the module, the test should then pass - and it does.<\/p>\n\n<pre><code class=\"language-plain\">Time: 24.76 seconds, Memory: 6.00MB\n\nOK (1 test, 3 assertions)\n<\/code><\/pre>\n\n<h2 id=\"ensure-that-the-pages-are-in-the-correct-order\">Ensure that the Pages are in the Correct Order<\/h2>\n\n<h3 id=\"writing-the-test\">Writing the Test<\/h3>\n\n<p>As we know that the correct content is being returned, we can now focus on displaying it in the correct order. We\u2019ll start again by adding a new test method and filling out the comments.<\/p>\n\n<pre><code class=\"language-php\">public function testResultsAreOrderedAlphabetically() {\n \/\/ Given I have multiple nodes with different titles.\n\n \/\/ When I view the pages list.\n\n \/\/ Then I should see pages in the correct order.\n}\n<\/code><\/pre>\n\n<p>To begin with this time, I\u2019ll create a number of different nodes and specify the title for each. These are intentionally in the incorrect order alphabetically so that we can see the test fail initially and then see it pass after making a change so we know that the change worked.<\/p>\n\n<pre><code class=\"language-php\">public function testResultsAreOrderedAlphabetically() {\n $this->drupalCreateNode(['title' => 'Page A']);\n $this->drupalCreateNode(['title' => 'Page D']);\n $this->drupalCreateNode(['title' => 'Page C']);\n $this->drupalCreateNode(['title' => 'Page B']);\n\n \/\/ When I view the pages list.\n\n \/\/ Then I should see pages in the correct order.\n}\n<\/code><\/pre>\n\n<p>We can use the same method as the previous test to get the returned IDs, using <code>views_get_view_result()<\/code> and <code>array_column()<\/code>, and assert that the returned node IDs match the expected node IDs in the specified order. Based on the defined titles, the order should be 1, 4, 3, 2.<\/p>\n\n<pre><code class=\"language-php\">public function testResultsAreOrderedAlphabetically() {\n $this->drupalCreateNode(['title' => 'Page A']);\n $this->drupalCreateNode(['title' => 'Page D']);\n $this->drupalCreateNode(['title' => 'Page C']);\n $this->drupalCreateNode(['title' => 'Page B']);\n\n $nids = array_column(views_get_view_result('pages'), 'nid');\n\n $this->assertEquals([1, 4, 3, 2], $nids);\n}\n<\/code><\/pre>\n\n<h3 id=\"running-the-test\">Running the Test<\/h3>\n\n<p>As expected the test fails, as the default sort criteria in the view orders the results by their created date.<\/p>\n\n<p>In the test output, we can see the returned results are in sequential order so the results array does not match the expected one.<\/p>\n\n<p>This would be particularly more complicated to test if I was using <code>drupalGet()<\/code> and having to parse the HTML, compared to getting the results as an array from the view programmatically.<\/p>\n\n<pre><code class=\"language-plain\">1) Drupal\\Tests\\tdd_dublin\\Functional\\PageListTest::testResultsAreOrderedAlphabetically\nFailed asserting that two arrays are equal.\n--- Expected\n+++ Actual\n@@ @@\n Array (\n- 0 => 1\n- 1 => 4\n- 2 => 3\n- 3 => 2\n+ 0 => '1'\n+ 1 => '2'\n+ 2 => '3'\n+ 3 => '4'\n )\n\nFAILURES!\nTests: 1, Assertions: 2, Failures: 1.\n<\/code><\/pre>\n\n<h3 id=\"updating-the-test\">Updating the Test<\/h3>\n\n<p>This can be fixed by removing the default sort criteria and adding a new one based on \"Content: Title\".<\/p>\n\n<p><img src=\"\/images\/blog\/tdd-drupal-4.png\" alt=\"\" title=\"\" class=\"with-border\" \/><\/p>\n\n<p>Again, once the view has been updated and exported, the test should pass - and it does.<\/p>\n\n<pre><code class=\"language-plain\">Time: 27.55 seconds, Memory: 6.00MB\n\nOK (1 test, 2 assertions)\n<\/code><\/pre>\n\n<h2 id=\"ensure-all-tests-still-pass\">Ensure all Tests Still Pass<\/h2>\n\n<p>Now we know that all the tests pass individually, all of the module tests should now be run to ensure that they all still pass and that there have been no regressions due to any of the changes.<\/p>\n\n<pre><code class=\"language-plain\">docker@cli:\/var\/www$ vendor\/bin\/phpunit -c core modules\/custom\/tdd_dublin\/tests\n\nTesting modules\/custom\/tdd_dublin\/tests\n...\n\nTime: 1.27 minutes, Memory: 6.00MB\n\nOK (3 tests, 6 assertions)\n<\/code><\/pre>\n\n<p>They all pass, so we be confident that the code works as expected, we can continue to refactor if needed, and if any changes are made to this module at a later date, we have the tests to ensure that any regressions are caught and fixed before deployment.<\/p>\n\n<h2 id=\"next-steps\">Next Steps<\/h2>\n\n<p>I\u2019ve started looking into whether some of the tests can be rewritten as kernel tests, which should result in quicker test execution. I will post any updated code to the <a href=\"https:\/\/packagist.org\/packages\/tightenco\/collect\">GitHub repository<\/a>, and will also do another blog post highlighting the differences between functional and kernel tests and the steps taken to do the conversion.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"testing"
|
||
,"tdd"
|
||
,"simpletest"
|
||
,"phpunit"
|
||
]
|
||
}, {
|
||
"title": "Publishing Sculpin Sites with GitHub Pages",
|
||
"path": "/articles/publishing-sculpin-sites-with-github-pages",
|
||
"is_draft": "false",
|
||
"created": "1499904000",
|
||
"excerpt": "How I moved my website to GitHub pages.",
|
||
"body": "<p class=\"text-center\"><img src=\"\/images\/blog\/jackson-octocat.png\" alt=\"\" \/><\/p>\n\n<p>Earlier this week I moved this site from my personal Linode server to <a href=\"https:\/\/pages.github.com\">GitHub Pages<\/a>.<\/p>\n\n<p>This made sense as I already kept the source code in <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\">on GitHub<\/a>, the issue was that GitHub Pages doesn\u2019t 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\u2019s just a case of how those files are added to GitHub.<\/p>\n\n<p>I\u2019ve seen different implementations of this, mostly where the Sculpin code is on one branch, and the generated HTML code is on a separate <code>gh-pages<\/code> or <code>master<\/code> branch (depending on your repository name). I\u2019m not fond of this approach as it means automatically checking out and merging branches which can get messy, and also it\u2019s weird to look at a repo\u2019s branches page and see one branch maybe tens or hundreds of commits both ahead and behind the default branch.<\/p>\n\n<p>This has been made simpler and tidier now that we can use a <code>docs<\/code> directory within the repository to serve content.<\/p>\n\n<p><img\n src=\"\/images\/blog\/github-pages.png\"\n alt=\"\"\n class=\"is-centered\"\n style=\"margin-top: 20px; margin-bottom: 20px\"\n><\/p>\n\n<p>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.<\/p>\n\n<p>To simplify this, I\u2019ve added a new <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\/blob\/master\/publish.sh\">publish.sh script<\/a> into my repository to automate the sites. This is how it currently looks:<\/p>\n\n<pre><code class=\"language-bash\">#!\/usr\/bin\/env bash\n\nSITE_ENV=\"prod\"\n\n# Remove the existing docs directory, build the site and create the new\n# docs directory.\nrm -rf .\/docs\nvendor\/bin\/sculpin generate --no-interaction --clean --env=${SITE_ENV}\ntouch output_${SITE_ENV}\/.nojekyll\nmv output_${SITE_ENV} docs\n\n# Ensure the correct Git variables are used.\ngit config --local user.name 'Oliver Davies'\ngit config --local user.email oliver@oliverdavies.uk\n\n# Add, commit and push the changes.\ngit add --all docs\ngit commit -m 'Build.'\ngit push origin HEAD\n<\/code><\/pre>\n\n<p>This begins by removing the deleting the existing <code>docs<\/code> directory and re-generating the site with the specified environment. Then I add a <code>.nojekyll<\/code> file and rename the output directory to replace <code>docs<\/code>.<\/p>\n\n<p>Now the changes can be added, committed and pushed. Once pushed, the new code is automatically served by GitHub Pages.<\/p>\n\n<h2 id=\"https\">HTTPS<\/h2>\n\n<p>GitHub Pages unfortunately does <a href=\"https:\/\/github.com\/blog\/2186-https-for-github-pages\">not support HTTPS for custom domains<\/a>.<\/p>\n\n<p>As the site was previously using HTTPS, I didn\u2019t want to have to go back to HTTP, break any incoming links and lose any potential traffic. To continue using HTTPS, I decided to <a href=\"https:\/\/blog.cloudflare.com\/secure-and-fast-github-pages-with-cloudflare\">use Cloudflare<\/a> to serve the site via their CDN which does allow for HTTPS traffic.<\/p>\n\n<h2 id=\"next-steps\">Next Steps<\/h2>\n\n<ul>\n<li>Enable automatically running <code>publish.sh<\/code> when new changes are pushed to GitHub rather than running it manually. I was previously <a href=\"\/articles\/2015\/07\/21\/automating-sculpin-jenkins\">using Jenkins<\/a> and Fabric for this, though I\u2019m also going to look into using Travis to accomplish this.<\/li>\n<li>Add the pre-build steps such as running <code>composer install<\/code> and <code>yarn<\/code> to install dependencies, and <code>gulp<\/code> to create the front-end assets. This was previously done by Jenkins in my previous setup.<\/li>\n<\/ul>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"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\">Publishing your GitHub Pages site from a \/docs folder on your master branch<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/blog\/572-bypassing-jekyll-on-github-pages\">Bypassing Jekyll on GitHub Pages<\/a><\/li>\n<li><a href=\"https:\/\/blog.cloudflare.com\/secure-and-fast-github-pages-with-cloudflare\">Secure and fast GitHub Pages with CloudFlare<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["sculpin"
|
||
,"php"
|
||
,"github"
|
||
]
|
||
}, {
|
||
"title": "Introducing the Drupal Meetups Twitterbot",
|
||
"path": "/articles/introducing-the-drupal-meetups-twitterbot",
|
||
"is_draft": "false",
|
||
"created": "1496966400",
|
||
"excerpt": "I\u2019ve written a twitterbot for promoting Drupal meetups.",
|
||
"body": "<p class=\"text-center\"><img src=\"\/images\/blog\/drupal-meetups-twitterbot.png\" alt=\"\" \/><\/p>\n\n<p>The <a href=\"https:\/\/github.com\/opdavies\/drupal-meetups-twitterbot\">Drupal Meetups Twitterbot<\/a> is a small project that I worked on a few months ago, but hadn't got around to promoting yet. It\u2019s intention is to provide <a href=\"https:\/\/twitter.com\/drupal_meetups\">one Twitter account<\/a> where people can get the up to date news from various Drupal meetups.<\/p>\n\n<p>It works by having a whitelist of <a href=\"https:\/\/github.com\/opdavies\/drupal-meetups-twitterbot\/blob\/master\/bootstrap\/config.php\">Twitter accounts and hashtags<\/a> to search for, uses <a href=\"https:\/\/www.jublo.net\/projects\/codebird\/php\">Codebird<\/a> to query the Twitter API and retweets any matching tweets on a scheduled basis.<\/p>\n\n<p>If you would like your meetup group to be added to the list of searched accounts, please <a href=\"https:\/\/github.com\/opdavies\/drupal-meetups-twitterbot\/issues\/new\">open an issue<\/a> on the GitHub repo.<\/p>\n",
|
||
"tags": ["twitter"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Turning Your Custom Drupal Module into a Feature",
|
||
"path": "/articles/turning-drupal-module-into-feature",
|
||
"is_draft": "false",
|
||
"created": "1495238400",
|
||
"excerpt": "How to turn a custom Drupal module into a Feature.",
|
||
"body": "<p>Yesterday I was fixing a bug in an inherited Drupal 7 custom module, and I decided that I was going to add some tests to ensure that the bug was fixed and doesn\u2019t get accidentially re-introduced in the future. The test though required me to have a particular content type and fields which are specific to this site, so weren\u2019t present within the standard installation profile used to run tests.<\/p>\n\n<p>I decided to convert the custom module into a <a href=\"https:\/\/www.drupal.org\/project\/features\">Feature<\/a> so that the content type and it\u2019s fields could be added to it, and therefore present on the testing site once the module is installed.<\/p>\n\n<p>To do this, I needed to expose the module to the Features API.<\/p>\n\n<p>All that\u2019s needed is to add this line to the <code>mymodule.info<\/code> file:<\/p>\n\n<pre><code class=\"language-ini\">features[features_api][] = api:2\n<\/code><\/pre>\n\n<p>After clearing the cache, the module is now visible in the Features list - and ready to have the appropriate configuration added to it.<\/p>\n\n<p><img src=\"\/images\/blog\/custom-module-as-a-feature.png\" alt=\"'The features list showing the custom module'\" \/><\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"features"
|
||
]
|
||
}, {
|
||
"title": "DrupalCamp Bristol 2017 - Early Bird Tickets, Call for Sessions, Sponsors",
|
||
"path": "/articles/drupalcamp-bristol-early-bird-tickets-sessions-sponsors",
|
||
"is_draft": "false",
|
||
"created": "1494806400",
|
||
"excerpt": "In less than two months time, DrupalCamp Bristol will be back for our third year.",
|
||
"body": "<p class=\"text-center\"><img src=\"\/images\/blog\/drupalcamp-bristol-17-logo.jpg\" alt=\"DrupalCamp Bristol 2017 logo\" \/><\/p>\n\n<p>In less than two months time, <a href=\"https:\/\/2017.drupalcampbristol.co.uk\">DrupalCamp Bristol<\/a> will be back for our third year! (July seems to come around quicker each year). This is this year\u2019s schedule and venues:<\/p>\n\n<ul>\n<li>30th June - CXO (Business) day - <a href=\"http:\/\/www.watershed.co.uk\">Watershed<\/a><\/li>\n<li>1st July - Developer conference - <a href=\"http:\/\/www.bris.ac.uk\/chemistry\">University of Bristol, School of Chemistry<\/a><\/li>\n<li>2nd July - Contribution sprints - Venue TBC<\/li>\n<\/ul>\n\n<p>Today we announced <a href=\"http:\/\/emmakarayiannis.com\">Emma Karayiannis<\/a> as our Saturday keynote speaker, and we\u2019ll be announcing some of the other speakers later this week.<\/p>\n\n<p>Not submitted your session yet? The <a href=\"https:\/\/2017.drupalcampbristol.co.uk\/#block-dcb2017-page-title\">session submissions<\/a> are open until May 31st. We\u2019re 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 <a href=\"mailto:speakers@drupalcampbristol.co.uk\">send us an email<\/a> or ping us on <a href=\"https:\/\/twitter.com\/DrupalCampBris\">Twitter<\/a>.<\/p>\n\n<p>Not spoken at a DrupalCamp before? No problem. We\u2019re looking for both new and experienced speakers, and have both long (45 minutes) and short (20 minutes) talk slots available.<\/p>\n\n<p>Not bought your tickets yet? <a href=\"https:\/\/www.eventbrite.co.uk\/e\/drupalcamp-bristol-2017-tickets-33574193316#ticket\">Early bird tickets<\/a> 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.<\/p>\n\n<p>We still have <a href=\"https:\/\/2017.drupalcampbristol.co.uk\/sponsorship\">sponsorships opportunities<\/a> available (big thanks to <a href=\"https:\/\/microserve.io\">Microserve<\/a>, <a href=\"https:\/\/www.deeson.co.uk\">Deeson<\/a> and <a href=\"http:\/\/www.proctors.co.uk\">Proctors<\/a>) 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.<\/p>\n\n<p>Any other questions? Take a look at <a href=\"https:\/\/2017.drupalcampbristol.co.uk\">our website<\/a> or get in touch via <a href=\"https:\/\/twitter.com\/DrupalCampBris\">Twitter<\/a> or <a href=\"mailto:info@drupalcampbristol.co.uk\">email<\/a>.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drupalcamp"
|
||
,"drupalcamp-bristol"
|
||
]
|
||
}, {
|
||
"title": "Updating Override Node Options Tests",
|
||
"path": "/articles/updating-override-node-options-tests",
|
||
"is_draft": "true",
|
||
"created": "1493942400",
|
||
"excerpt": "",
|
||
"body": "<p>Recently, I reviewed <a href=\"https:\/\/www.drupal.org\/node\/974730\">a patch<\/a> in the <a href=\"https:\/\/www.drupal.org\/project\/override_node_options\">Override Node Options<\/a> module issue queue. For those not familiar with it, the module adds extra permissions for node options like \"authored by\" and \"published on\" which are normally only available to users with the <code>administer nodes<\/code> permission. What the patch does is to optionally add another set of permissions that enable options for all content types - e.g. \"override published option for all node types\", in addition to or instead of the content type specific ones.<\/p>\n\n<p>It was quite an old issue and the latest patch needed to be re-rolled due to merge conflicts, but the existing tests still passed. Though as no new tests were added for the new functionality, these needed to be added before I committed it.<\/p>\n\n<h2 id=\"reviewing-the-existing-tests\">Reviewing the Existing Tests<\/h2>\n\n<p>The first thing to do was to run the existing tests and check that they still passed. I do this on the command line by typing <code>php scripts\/run-tests.sh --class OverrideNodeOptionsTestCase<\/code>.<\/p>\n\n<pre><code class=\"language-markup\">Drupal test run\n---------------\n\nTests to be run:\n - Override node options (OverrideNodeOptionsTestCase)\n\nTest run started:\n Saturday, April 29, 2017 - 14:44\n\nTest summary\n------------\n\nOverride node options 142 passes, 0 fails, 0 exceptions, and 38 debug messages\n\nTest run duration: 32 sec\n<\/code><\/pre>\n\n<p>After confirming that the existing tests still passed, I reviewed them to see what could be re-used.<\/p>\n\n<p>This is one of the original tests:<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Test the 'Authoring information' fieldset.\n *\/\nprotected function testNodeOptions() {\n $this->adminUser = $this->drupalCreateUser(array(\n 'create page content',\n 'edit any page content',\n 'override page published option',\n 'override page promote to front page option',\n 'override page sticky option',\n 'override page comment setting option',\n ));\n $this->drupalLogin($this->adminUser);\n\n $fields = array(\n 'status' => (bool) !$this->node->status,\n 'promote' => (bool) !$this->node->promote,\n 'sticky' => (bool) !$this->node->sticky,\n 'comment' => COMMENT_NODE_OPEN,\n );\n $this->drupalPost('node\/' . $this->node->nid . '\/edit', $fields, t('Save'));\n $this->assertNodeFieldsUpdated($this->node, $fields);\n\n $this->drupalLogin($this->normalUser);\n $this->assertNodeFieldsNoAccess($this->node, array_keys($fields));\n}\n<\/code><\/pre>\n\n<p>The first part of the test is creating and logging in a user with some content type specific override permissions (<code>$this->adminUser<\/code>), and then testing that the fields were updated when the node is saved. The second part is testing that the fields are not visible for a normal user without the extra permissions (<code>$this->normalUser<\/code>), which is created in the <code>setUp()<\/code> class' method.<\/p>\n\n<p>To test the new \"all types\" permissions, I created another user to test against called <code>$generalUser<\/code> and run the first part of the tests in a loop.<\/p>\n\n<h2 id=\"beginning-to-refactor-the-tests\">Beginning to Refactor the Tests<\/h2>\n\n<p>With the tests passing, I was able to start refactoring.<\/p>\n\n<pre><code class=\"language-php\">\/\/ Create a new user with content type specific permissions.\n$specificUser = $this->drupalCreateUser(array(\n 'create page content',\n 'edit any page content',\n 'override page published option',\n 'override page promote to front page option',\n 'override page sticky option',\n 'override page comment setting option',\n));\n\nforeach (array($specificUser) as $account) {\n $this->drupalLogin($account);\n\n \/\/ Test all the things.\n ...\n}\n<\/code><\/pre>\n\n<p>I started with a small change, renaming <code>$this->adminUser<\/code> to <code>$specificUser<\/code> to make it clearer what permissions it had, and moving the tests into a loop so that the tests can be repeated for both users.<\/p>\n\n<p>After that change, I ran the tests again to check that everything still worked.<\/p>\n\n<h2 id=\"adding-failing-tests\">Adding Failing Tests<\/h2>\n\n<p>The next step is to start testing the new permissions.<\/p>\n\n<pre><code class=\"language-php\">...\n\n$generalUser = $this->drupalCreateUser(array());\n\nforeach (array($specificUser, $generalUser) as $account) {\n $this->drupalLogin($account);\n\n \/\/ Test all the things.\n}\n<\/code><\/pre>\n\n<p>I added a new <code>$generalUser<\/code> to test the general permissions and added to the loop, but in order to see the tests failing intially I assigned it no permissions. When running the tests again, 6 tests have failed.<\/p>\n\n<pre><code class=\"language-markup\">Test summary\n------------\n\nOverride node options 183 passes, 6 fails, 0 exceptions, and 49 debug messages\n\nTest run duration: 28 sec\n<\/code><\/pre>\n\n<p>Then it was a case of re-adding more permissions to the user and seeing the number of failures decrease, confirming that the functionality was working correctly.<\/p>\n\n<p>TODO: Add another example.<\/p>\n\n<h2 id=\"gotchas\">Gotchas<\/h2>\n\n<p>There was a bug that I found where a permission was added, but wasn't used within the implementation code. After initially expecting the test to pass after adding the permission to <code>$generalUser<\/code> and the test still failed, I noticed that the<\/p>\n\n<p>This was fixed by adding the extra code into <code>override_node_options.module<\/code>.<\/p>\n\n<pre><code class=\"language-diff\">- $form['comment_settings']['#access'] |= user_access('override ' . $node->type . ' comment setting option');\n+ $form['comment_settings']['#access'] |= user_access('override ' . $node->type . ' comment setting option') || user_access('override all comment setting option');\n<\/code><\/pre>\n\n<p>The other issue that I found was within <code>testNodeRevisions<\/code>. <code>assertNodeFieldsUpdated()<\/code> was failing after being put in a loop as the <code>vid<\/code> was not the same as what was expected.<\/p>\n\n<p>Note: You can get more verbose output from <code>run-tests.sh<\/code> by adding the <code>--verbose<\/code> option.<\/p>\n\n<blockquote>\n <p>Node vid was updated to '3', expected 2.<\/p>\n<\/blockquote>\n\n<pre><code class=\"language-diff\">- $fields = array(\n- 'revision' => TRUE,\n- );\n- $this->drupalPost('node\/' . $this->node->nid . '\/edit', $fields, t('Save'));\n- $this->assertNodeFieldsUpdated($this->node, array('vid' => $this->node->vid + 1));\n+ $generalUser = $this->drupalCreateUser(array(\n+ 'create page content',\n+ 'edit any page content',\n+ 'override all revision option',\n+ ));\n+\n+ foreach (array($specificUser, $generalUser) as $account) {\n+ $this->drupalLogin($account);\n+\n+ \/\/ Ensure that we have the latest node data.\n+ $node = node_load($this->node->nid, NULL, TRUE);\n+\n+ $fields = array(\n+ 'revision' => TRUE,\n+ );\n+ $this->drupalPost('node\/' . $node->nid . '\/edit', $fields, t('Save'));\n+ $this->assertNodeFieldsUpdated($node, array('vid' => $node->vid + 1));\n+ }\n<\/code><\/pre>\n\n<p>The crucial part of this change was the addition of <code>$node = node_load($this->node->nid, NULL, TRUE);<\/code> to ensure that the latest version of the node was loaded during each loop.<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<ul>\n<li>Ensure that the existing tests were passing before starting to refactor.<\/li>\n<li>Start with small changes and continue to run the tests to ensure that nothing has broken.<\/li>\n<li>After the first change, I committed it as <code>WIP: Refactoring tests<\/code>, and used <code>git commit --amend --no-edit<\/code> to amend that commit each time I had refactored another test. After the last refactor, I updated the commit message.<\/li>\n<li>It\u2019s important to see tests failing before making them pass. This was achieved by initially assigning no permissions to <code>$generalUser<\/code> so that the fails failed and then added permissions and re-run the tests to ensure that the failure count decreased with each new permission.<\/li>\n<\/ul>\n\n<p>With the refactoring complete, the number of passing tests increased from 142 to 213.<\/p>\n\n<pre><code class=\"language-markup\">Override node options 213 passes, 0 fails, 0 exceptions, and 60 debug messages\n\nTest run duration: 25 sec\n<\/code><\/pre>\n\n<p><img src=\"\/images\/blog\/override-node-options-refactor-tests-new-passing.png\" alt=\"\"><\/p>\n\n<p><a href=\"https:\/\/www.drupal.org\/files\/issues\/interdiff_25712.txt\">Here<\/a> are my full changes from the previous patch, where I added the new tests as well as some small refactors.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-modules"
|
||
,"drupal-planet"
|
||
,"testing"
|
||
,"drafts"
|
||
]
|
||
}, {
|
||
"title": "Fixing Drupal SimpleTest issues inside Docker Containers",
|
||
"path": "/articles/fixing-drupal-simpletest-docker",
|
||
"is_draft": "false",
|
||
"created": "1493942400",
|
||
"excerpt": "How I managed to get my Drupal SimpleTest tests to run and pass within Docker containers.",
|
||
"body": "<p>I\u2019ve been a Drupal VM user for a long time, but lately I\u2019ve 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.<\/p>\n\n<p>Here\u2019s an excerpt from my <code>docker-compose.yml<\/code> file:<\/p>\n\n<p><strong>TL;DR<\/strong> You need to include the name of your web server container as the <code>--url<\/code> option to <code>run-scripts.php<\/code>.<\/p>\n\n<p>I\u2019ve been a <a href=\"https:\/\/www.drupalvm.com\">Drupal VM<\/a> user for a long time, but lately I\u2019ve been using a combination Drupal VM and <a href=\"https:\/\/www.docker.com\">Docker<\/a> 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.<\/p>\n\n<p>Here\u2019s an excerpt from my <code>docker-compose.yml<\/code> file:<\/p>\n\n<pre><code class=\"language-yaml\">services:\n php:\n image: wodby\/drupal-php:5.6\n volumes:\n - .\/repo:\/var\/www\/html\n\n nginx:\n image: wodby\/drupal-nginx:7-1.10\n environment:\n NGINX_BACKEND_HOST: php\n NGINX_SERVER_ROOT: \/var\/www\/html\/web\n ports:\n - \"80:80\"\n volumes_from:\n - php\n...\n<\/code><\/pre>\n\n<p>Nginx and PHP-FPM are running in separate containers, the volumes are shared across both and the Nginx backend is set to use the <code>php<\/code> container.<\/p>\n\n<p>This is the command that I was using to run the tests:<\/p>\n\n<pre><code class=\"language-bash\">$ docker-compose run --rm \\\n -w \/var\/www\/html\/web \\\n php \\\n php scripts\/run-tests.sh \\\n --php \/usr\/local\/bin\/php \\\n --class OverrideNodeOptionsTestCase\n<\/code><\/pre>\n\n<p>This creates a new instance of the <code>php<\/code> container, sets the working directory to my Drupal root and runs Drupal\u2019s <code>run-tests.sh<\/code> script with some arguments. In this case, I'm running the <code>OverrideNodeOptionsTestCase<\/code> class for the override_node_options tests. Once complete, the container is deleted because of the <code>--rm<\/code> option.<\/p>\n\n<p>This resulted in 60 of the 112 tests failing, whereas they all passed when run within a Drupal VM instance.<\/p>\n\n<pre><code class=\"language-markup\">Test summary\n------------\n\nOverride node options 62 passes, 60 fails, 29 exceptions, and 17 debug messages\n\nTest run duration: 2 min 25 sec\n<\/code><\/pre>\n\n<p>Running the tests again with the<code>--verbose<\/code> option, I saw this message appear in the output below some of the failing tests:<\/p>\n\n<blockquote>\n <p>simplexml_import_dom(): Invalid Nodetype to import<\/p>\n<\/blockquote>\n\n<p>**Up\nAfter checking that I had all of the required PHP extensions installed, I ran <code>docker-compose exec php bash<\/code> to connect to the <code>php<\/code> container and ran <code>curl http:\/\/localhost<\/code> to check the output. Rather than seeing the HTML for the site, I got this error message:<\/p>\n\n<blockquote>\n <p>curl: (7) Failed to connect to localhost port 80: Connection refused<\/p>\n<\/blockquote>\n\n<p>Whereas <code>curl http:\/\/nginx<\/code> returns the HTML for the page, so included it with the <code>--url<\/code> option to <code>run-tests.sh<\/code>, and this resulted in my tests all passing.<\/p>\n\n<pre><code class=\"language-bash\">$ docker-compose run --rm \\\n -w \/var\/www\/html\/web \\\n php \\\n php scripts\/run-tests.sh \\\n --php \/usr\/local\/bin\/php \\\n --url http:\/\/nginx \\\n --class OverrideNodeOptionsTestCase\n<\/code><\/pre>\n\n<pre><code class=\"language-markup\">Test summary\n------------\n\nOverride node options 121 passes, 0 fails, 0 exceptions, and 34 debug messages\n\nTest run duration: 2 min 31 sec\n<\/code><\/pre>\n\n<p><strong>Note:<\/strong> In this example I have separate <code>nginx<\/code> and <code>php<\/code> containers, but I've tried and had the same issue when running Nginx and PHP-FPM in the same container - e.g. called <code>app<\/code> - and still needed to add <code>--url http:\/\/app<\/code> in order for the tests to run successfully.<\/p>\n\n<p>I don\u2019t know if this issue is macOS specfic (I know that <a href=\"https:\/\/www.drupal.org\/drupalorg\/docs\/drupal-ci\">Drupal CI<\/a> is based on Docker, and I don\u2019t know if it\u2019s an issue) but I\u2019m 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\u2019m also going to test this with PHPUnit tests with Drupal 8.<\/p>\n",
|
||
"tags": ["docker"
|
||
,"drupal"
|
||
,"drupal-planet"
|
||
,"simpletest"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Nginx Redirects With Query String Arguments",
|
||
"path": "/articles/nginx-redirects-with-query-string-arguments",
|
||
"is_draft": "false",
|
||
"created": "1485820800",
|
||
"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.",
|
||
"body": "<p>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 <code>example.com<\/code> domain to the canonical <code>www<\/code> subdomain.<\/p>\n\n<pre><code class=\"language-nginx\">server {\n listen 80;\n\n server_name example.com;\n server_name my-old-domain.com;\n server_name www.my-old-domain.com;\n\n return 301 https:\/\/www.example.com$uri;\n}\n<\/code><\/pre>\n\n<p>It also redirects the URI value, e.g. from <code>http:\/\/example.com\/test<\/code> to <code>http:\/\/example.com\/test<\/code>, but I noticed recently though that any the query string would be lost - e.g. <code>http:\/\/example.com\/?test<\/code> would redirect to <code>http:\/\/www.example.com<\/code> and the <code>?test<\/code> 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.<\/p>\n\n<p>This was fixed by making a small change to my <code>return<\/code> statement.<\/p>\n\n<p>Before:<\/p>\n\n<pre><code class=\"language-nginx\">return 301 https:\/\/www.example.com$uri;\n<\/code><\/pre>\n\n<p>After:<\/p>\n\n<pre><code class=\"language-nginx\">return 301 https:\/\/www.example.com$uri$is_args$args;\n<\/code><\/pre>\n\n<p><code>$is_args<\/code> is an empty string if there are no arguments, or a <code>?<\/code> to signify the start of the query string. <code>$args<\/code> then adds the arguments (<code>$query_string<\/code> could also be used with the same result).<\/p>\n\n<p>Here is an demo of it working on this website:<\/p>\n\n<p><img src=\"\/images\/blog\/nginx-redirect-with-args.gif\" alt=\"\" \/><\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Query_string\">Query string<\/a><\/li>\n<li><a href=\"http:\/\/nginx.org\/en\/docs\/http\/ngx_http_core_module.html\">Nginx ngx_http_core_module<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["nginx"
|
||
]
|
||
}, {
|
||
"title": "Easier Sculpin Commands with Composer and NPM Scripts",
|
||
"path": "/articles/easier-sculpin-commands-with-composer-and-npm-scripts",
|
||
"is_draft": "false",
|
||
"created": "1483747200",
|
||
"excerpt": "In this video, I show you how I've simplied my Sculpin and Gulp workflow using custom Composer and NPM scripts.",
|
||
"body": "<p>In this video, I show you how I've simplied my Sculpin and Gulp workflow using custom Composer and NPM scripts.<\/p>\n\n<p>My website includes several various command line tools - e.g. <a href=\"https:\/\/sculpin.io\">Sculpin<\/a>, <a href=\"http:\/\/gulpjs.com\">Gulp<\/a> and <a href=\"http:\/\/behat.org\">Behat<\/a> - 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 <code>.\/vendor\/bin\/sculpin generate --watch --server --clean --no-interaction<\/code>. Typing this repeatedly is time consuming and could be easily mis-typed, forgotten or confused with other commands.<\/p>\n\n<p>In this video, I show you how I've simplied my Sculpin and Gulp workflow using custom Composer and NPM scripts.<\/p>\n\n<div class=\"embed-container\">\n <iframe width=\"560\" height=\"315\" src=\"https:\/\/www.youtube.com\/embed\/eiWDV_63yCQ\" frameborder=\"0\" allowfullscreen><\/iframe>\n<\/div>\n\n<h2 id=\"scripts\">Scripts<\/h2>\n\n<p>Here are the scripts that I\u2019m using - they are slightly different from those in the video. I use the <code>--generate<\/code> and <code>--watch<\/code> options for Sculpin and the <code>gulp watch<\/code> command for NPM. I had to change these before the recording as I was using the <a href=\"https:\/\/github.com\/paxtonhare\/demo-magic\">demo magic<\/a> script to run the commands, and existing from a watch session was also ending the script process.<\/p>\n\n<h3 id=\"composer.json\">composer.json<\/h3>\n\n<pre><code class=\"language-json\">\"scripts\": {\n \"clean\": \"rm -rf output_*\/\",\n \"dev\": \"sculpin generate --clean --no-interaction --server --watch\",\n \"production\": \"sculpin generate --clean --no-interaction --env='prod' --quiet\"\n}\n<\/code><\/pre>\n\n<p>Run with <code>composer run <name><\/code>, e.g. <code>composer run dev<\/code>.<\/p>\n\n<h3 id=\"package.json\">package.json<\/h3>\n\n<pre><code class=\"language-json\">\"scripts\": {\n \"init\": \"yarn && bower install\",\n \"dev\": \"gulp watch\",\n \"production\": \"gulp --production\"\n}\n<\/code><\/pre>\n\n<p>Run with <code>npm run <name><\/code>, e.g. <code>npm run production<\/code>.<\/p>\n\n<p>You can also take a look at the full <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\/blob\/master\/composer.json\">composer.json<\/a> and <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\/blob\/master\/package.json\">package.json<\/a> files within my site repository on <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\">GitHub<\/a>.<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/getcomposer.org\/doc\/04-schema.md#scripts\">Composer scripts<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\/blob\/master\/composer.json\">oliverdavies.uk composer.json<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\/blob\/master\/package.json\">oliverdavies.uk package.json<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["composer"
|
||
,"gulp"
|
||
,"sculpin"
|
||
]
|
||
}, {
|
||
"title": "Drupal VM Generator 2.9.1 Released",
|
||
"path": "/articles/drupal-vm-generator-updates",
|
||
"is_draft": "false",
|
||
"created": "1483056000",
|
||
"excerpt": "I\u2019ve released some new versions of the Drupal VM Generator.",
|
||
"body": "<p>The main updates are:<\/p>\n\n<ul>\n<li>Fixed an <code>InvalidResponseException<\/code> that was thrown from within the <code>boolean_as_string<\/code> Twig filter from the opdavies\/twig-extensions library when the <code>config:generate<\/code> command was run in non-interactive mode.<\/li>\n<li>Adding a working test suite for the existing commands, using PhpUnit and Symfony\u2019s Process component. This is now linked to <a href=\"https:\/\/travis-ci.org\/opdavies\/drupal-vm-generator\">Travis CI<\/a>, and the tests are run on each commit and pull request.<\/li>\n<li>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.<\/li>\n<\/ul>\n\n<h2 id=\"next-steps\">Next Steps<\/h2>\n\n<p>Currently the project is based on Drupal VM 3.0.0 which is an outdated version (<a href=\"https:\/\/github.com\/geerlingguy\/drupal-vm\/releases\/tag\/4.1.0\">4.1.0<\/a> 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.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-vm-generator"
|
||
,"releases"
|
||
]
|
||
}, {
|
||
"title": "Building Gmail Filters with PHP",
|
||
"path": "/articles/building-gmail-filters-in-php",
|
||
"is_draft": "false",
|
||
"created": "1468540800",
|
||
"excerpt": "How to use PHP to generate and export filters for Gmail.",
|
||
"body": "<p>Earlier this week I wrote a small PHP library called <a href=\"https:\/\/github.com\/opdavies\/gmail-filter-builder\">GmailFilterBuilder<\/a> that allows you to write Gmail filters in PHP and export them to XML.<\/p>\n\n<p>I was already aware of a Ruby library called <a href=\"https:\/\/github.com\/antifuchs\/gmail-britta\">gmail-britta<\/a> that does the same thing, but a) I\u2019m not that familiar with Ruby so the syntax wasn\u2019t that natural to me - it\u2019s been a while since I wrote any Puppet manifests, and b) it seemed like a interesting little project to work on one evening.<\/p>\n\n<p>The library contains two classes - <code>GmailFilter<\/code> which is used to create each filter, and <code>GmailFilterBuilder<\/code> that parses the filters and generates the XML using a <a href=\"http:\/\/twig.sensiolabs.org\">Twig<\/a> template.<\/p>\n\n<h2 id=\"usage\">Usage<\/h2>\n\n<p>For example:<\/p>\n\n<pre><code class=\"language-php\"># test.php\n\nrequire __DIR__ '\/vendor\/autoload.php';\n\nuse Opdavies\\GmailFilterBuilder\\Builder;\nuse Opdavies\\GmailFilterBuilder\\Filter;\n\n$filters = [];\n\n$filters[] = Filter::create()\n ->has('from:example@test.com')\n ->labelAndArchive('Test')\n ->neverSpam();\n\nnew Builder($filters);\n<\/code><\/pre>\n\n<p>In this case, an email from <code>example@test.com<\/code> would be archived, never marked as spam, and have a label of \"Test\" added to it.<\/p>\n\n<p>With this code written, and the GmailFilterBuilder library installed via Composer, I can run <code>php test.php<\/code> and have the XML written to the screen.<\/p>\n\n<p>This can also be written to a file - <code>php test.php > filters.xml<\/code> - which can then be imported into Gmail.<\/p>\n\n<h2 id=\"twig-extensions\">Twig Extensions<\/h2>\n\n<p>I also added a custom Twig extension that I moved into a separate <a href=\"https:\/\/packagist.org\/packages\/opdavies\/twig-extensions\">twig-extensions<\/a> library so that I and other people can re-use it in other projects.<\/p>\n\n<p>It\u2019s a simple filter that accepts a boolean and returns <code>true<\/code> or <code>false<\/code> as a string, but meant that I could remove three ternary operators from the template and replace them with the <code>boolean_string<\/code> filter.<\/p>\n\n<p>Before:<\/p>\n\n<div v-pre>\n\n<pre><code class=\"language-twig\">{{ filter.isArchive ? 'true' : 'false' }}\n<\/code><\/pre>\n\n<\/div>\n\n<p>After:<\/p>\n\n<div v-pre>\n\n<pre><code class=\"language-twig\">{{ filter.isArchive|boolean_string }}\n<\/code><\/pre>\n\n<\/div>\n\n<p>This can then be used to generate output like this, whereas having blank values would have resulted in errors when importing to Gmail.<\/p>\n\n<pre><code class=\"language-xml\"><apps:property name='shouldArchive' value='true'\/>\n<\/code><\/pre>\n\n<h2 id=\"example\">Example<\/h2>\n\n<p>For a working example, see my personal <a href=\"https:\/\/github.com\/opdavies\/gmail-filters\">gmail-filters<\/a> repository on GitHub.<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/packagist.org\/packages\/opdavies\/gmail-filter-builder\">The GmailFilterBuilder library on Packagist<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/opdavies\/gmail-filters\">My Gmail filters on GitHub<\/a><\/li>\n<li><a href=\"https:\/\/packagist.org\/packages\/opdavies\/twig-extensions\">My Twig Extensions on Packagist<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["php"
|
||
,"gmail"
|
||
]
|
||
}, {
|
||
"title": "Simplifying Drupal Migrations with xautoload",
|
||
"path": "/articles/simplifying-drupal-migrations-with-xautoload",
|
||
"is_draft": "false",
|
||
"created": "1462233600",
|
||
"excerpt": "How to use the xautoload module to autoload migration classes within your Drupal 7 migration modules.",
|
||
"body": "<p>How to use the <a href=\"https:\/\/www.drupal.org\/project\/xautoload\">xautoload<\/a> module to autoload migration classes within your Drupal 7 migration modules.<\/p>\n\n<h2 id=\"what-is-xautoload%3F\">What is xautoload?<\/h2>\n\n<p><a href=\"https:\/\/www.drupal.org\/project\/xautoload\">xautoload<\/a> is a Drupal module that enables the autoloading of PHP classes, in the same way that you would do so in a <a href=\"http:\/\/getcomposer.org\">Composer<\/a> based project such as Drupal 8 or Symfony.<\/p>\n\n<p>It supports both the <a href=\"http:\/\/www.php-fig.org\/psr\/psr-0\/\">PSR-0<\/a> and <a href=\"http:\/\/www.php-fig.org\/psr\/psr-4\/\">PSR-4<\/a> standards, as well as providing a wildcard syntax for Drupal\u2019s <code>file[]<\/code> syntax in .info files.<\/p>\n\n<p>To use it, download and enable it from Drupal.org as you would for any other module, and then add it as a dependency within your module. The xautoload project page suggests including a minimum version in this format:<\/p>\n\n<pre><code class=\"language-ini\">dependencies[] = xautoload (>= 7.x-5.0)\n<\/code><\/pre>\n\n<p>This will ensure that the version of xautoload is 7.x-5.0 or newer.<\/p>\n\n<h2 id=\"how-to-use-it\">How to use it<\/h2>\n\n<h3 id=\"wildcard-syntax-for-.info-files\">Wildcard syntax for .info files<\/h3>\n\n<p>Here is an example .info file for a migrate module.<\/p>\n\n<pre><code class=\"language-ini\">; foo_migrate.info\n\nname = Foo Migration\ncore = 7.x\npackage = Foo\n\nfiles[] = includes\/user.inc\nfiles[] = includes\/nodes\/article.inc\nfiles[] = includes\/nodes\/page.inc\n<\/code><\/pre>\n\n<p>In this example, each custom migration class is stored in it\u2019s own file within the <code>includes<\/code> directory, and each class needs to be loaded separately using the <code>files[] = filename<\/code> syntax.<\/p>\n\n<p>One thing that the xautoload module does to enable for the use of wildcards within this syntax. By using wildcards, the module file can be simplified as follows:<\/p>\n\n<pre><code class=\"language-ini\">files[] = includes\/**\/*.inc\n<\/code><\/pre>\n\n<p>This will load any .inc files within the <code>includes<\/code> directory as well as any sub-directories, like 'node' in the original example.<\/p>\n\n<p>This means that any new migration classes that are added will be automatically loaded, so you don\u2019t need to declare each include separately within foo_migrate.info again. The great thing about this approach is that it works with the existing directory and file structure.<\/p>\n\n<h3 id=\"use-the-psr-4-structure\">Use the PSR-4 structure<\/h3>\n\n<p>If you want to use the <a href=\"http:\/\/www.php-fig.org\/psr\/psr-4\/\">PSR-4<\/a> approach, you can do that too.<\/p>\n\n<p>In order to do so, you\u2019ll need to complete the following steps:<\/p>\n\n<ol>\n<li>Rename the <code>includes<\/code> directory to <code>src<\/code>.<\/li>\n<li>Ensure that there is one PHP class per file, and that the file extension is <code>.php<\/code> rather than <code>.inc<\/code>.<\/li>\n<li>Ensure that the name of the file matches the name of the class - <code>FooArticleNodeMigration<\/code> would be in a file called <code>FooArticleNodeMigration.php<\/code>.<\/li>\n<li>Add a namespace to each PHP file. This uses the same format as Drupal 8, including the machine name of the module. For example, <code>Drupal\\foo_migrate<\/code>.\n\n<ul>\n<li>If the class is within a sub-directory, then this will also need to be included within the namespace - e.g. <code>Drupal\\foo_migrate\\Node<\/code>.<\/li>\n<li>You\u2019ll also need to import any class names that you are referencing, including class names that are you extending, by adding <code>use<\/code> statements at the top of the file. You may be able to prefix it with <code>\\<\/code> instead (e.g. <code>\\DrupalNode6Migration<\/code>), but I prefer to use imports.<\/li>\n<\/ul><\/li>\n<\/ol>\n\n<p>Now your class may look something like this:<\/p>\n\n<pre><code class=\"language-php\"><?php\n\nnamespace Drupal\\foo_migrate\\Node;\n\nuse DrupalNode6Migration;\n\nclass FooArticleNodeMigration extends DrupalNode6Migration {\n ...\n}\n<\/code><\/pre>\n\n<p>With these steps completed, any imports within your .info file can be removed as they are no longer needed and any classes will be loaded automatically.<\/p>\n\n<p>Within <code>foo_migrate.migrate.inc<\/code>, I can now reference any class names using their full namespace:<\/p>\n\n<pre><code class=\"language-php\">$node_arguments['ArticleNode'] = array(\n 'class_name' => 'Drupal\\foo_migrate\\Node\\FooArticleNodeMigration',\n 'source_type' => 'story',\n 'destination_type' => 'article',\n);\n<\/code><\/pre>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/www.drupal.org\/project\/xautoload\">xautoload module<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/project\/migrate\">migrate module<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/project\/migrate_d2d\">migrate_d2d module<\/a><\/li>\n<li><a href=\"http:\/\/www.php-fig.org\/psr\/psr-0\/\">PSR-0<\/a><\/li>\n<li><a href=\"http:\/\/www.php-fig.org\/psr\/psr-4\/\">PSR-4<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["autoloading"
|
||
,"drupal"
|
||
,"drupal-planet"
|
||
,"drupal-7"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Announcing the Drupal VM Generator",
|
||
"path": "/articles/announcing-the-drupal-vm-generator",
|
||
"is_draft": "false",
|
||
"created": "1455494400",
|
||
"excerpt": "For the past few weeks, I\u2019ve been working on a personal side project based on Drupal VM - the Drupal VM Generator.",
|
||
"body": "<p>For the past few weeks, I\u2019ve been working on a personal side project based on Drupal VM. It\u2019s called the <a href=\"https:\/\/github.com\/opdavies\/drupal-vm-generator\">Drupal VM Generator<\/a>, and over the weekend I\u2019ve added the final features and fixed the remaining issues, and tagged the 1.0.0 release.<\/p>\n\n<p><img src=\"\/images\/blog\/drupalvm-generate-repo.png\" alt=\"\" \/><\/p>\n\n<h2 id=\"what-is-drupal-vm%3F\">What is Drupal VM?<\/h2>\n\n<p><a href=\"http:\/\/www.drupalvm.com\">Drupal VM<\/a> is a project created and maintained by <a href=\"http:\/\/www.jeffgeerling.com\">Jeff Geerling<\/a>. It\u2019s a <a href=\"http:\/\/www.vagrantup.com\">Vagrant<\/a> virtual machine for Drupal development that is provisioned using <a href=\"https:\/\/www.ansible.com\">Ansible<\/a>.<\/p>\n\n<p>What is different to a regular Vagrant VM is that uses a file called <code>config.yml<\/code> to configure the machine. Settings such as <code>vagrant_hostname<\/code>, <code>drupalvm_webserver<\/code> and <code>drupal_core_path<\/code> are stored as YAML and passed into the <code>Vagrantfile<\/code> and the <code>playbook.yml<\/code> file which is used when the Ansible provisioner runs.<\/p>\n\n<p>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.<\/p>\n\n<h2 id=\"what-does-the-drupal-vm-generator-do%3F\">What does the Drupal VM Generator do?<\/h2>\n\n<blockquote>\n <p>The Drupal VM Generator is a Symfony application that allows you to quickly create configuration files that are minimal and use-case specific.<\/p>\n<\/blockquote>\n\n<p>Drupal VM comes with an <a href=\"https:\/\/github.com\/geerlingguy\/drupal-vm\/blob\/master\/example.config.yml\">example.config.yml file<\/a> that shows all of the default variables and their values. When I first started using it, I\u2019d make a copy of <code>example.config.yml<\/code>, rename it to <code>config.yml<\/code> and edit it as needed, but a lot of the examples aren\u2019t needed for every use case. If you\u2019re using Nginx as your webserver, then you don\u2019t need the Apache virtual hosts. If you are not using Solr on this project, then you don\u2019t need the Solr variables.<\/p>\n\n<p>For a few months, I\u2019ve kept and used boilerplace versions of <code>config.yml<\/code> - 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.<\/p>\n\n<p>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 <a href=\"http:\/\/symfony.com\/doc\/current\/components\/console\/introduction.html\">Console component<\/a> to collect input from the user, <a href=\"http:\/\/twig.sensiolabs.org\">Twig<\/a> to generate the file, the <a href=\"http:\/\/symfony.com\/doc\/current\/components\/filesystem\/introduction.html\">Filesystem component<\/a> to write it.<\/p>\n\n<p>Based on the options passed to it and\/or answers that you provide, it generates a custom, minimal <code>config.yml<\/code> file for your project.<\/p>\n\n<p>Here\u2019s an example of it in action:<\/p>\n\n<p><img src=\"\/images\/blog\/drupalvm-generate-example-2.gif\" alt=\"'An animated gif showing the interaction process and the resulting config.yml file'\" \/><\/p>\n\n<p>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.<\/p>\n<code data-gist-id=\"24e569577ca4b72f049d\" data-gist-file=\"with-options.sh\"><\/code>\n<h2 id=\"where-do-i-get-it%3F\">Where do I get it?<\/h2>\n\n<p>The project is hosted on <a href=\"https:\/\/github.com\/opdavies\/drupal-vm-generator\">GitHub<\/a>, and there are installation instructions within the <a href=\"https:\/\/github.com\/opdavies\/drupal-vm-generator\/blob\/master\/README.md#installation\">README<\/a>.<\/p>\n\n<div class=\"github-card\" data-github=\"opdavies\/drupal-vm-generator\" data-width=\"400\" data-height=\"\" data-theme=\"default\"><\/div>\n\n<p>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\u2019m also wanting to upload it to Packagist so that it can be included if you manage your projects with Composer.<\/p>\n\n<p>Please log any bugs or feature requests in the <a href=\"https:\/\/github.com\/opdavies\/drupal-vm-generator\/issues\">GitHub issue tracker<\/a>, and I\u2019m more than happy to receive pull requests.<\/p>\n\n<p>If you\u2019re interested in contributing, please feel free to fork the repository and start doing so, or contact me with any questions.<\/p>\n\n<p><strong>Update 17\/02\/16:<\/strong> The autoloading issue is now fixed if you require the package via Composer, and this has been tagged as the <a href=\"https:\/\/github.com\/opdavies\/drupal-vm-generator\/releases\/tag\/1.0.1\">1.0.1 release<\/a><\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drupal-vm"
|
||
,"drupal-vm-generator"
|
||
,"symfony"
|
||
]
|
||
}, {
|
||
"title": "Programmatically Load an Entityform in Drupal 7",
|
||
"path": "/articles/entityform",
|
||
"is_draft": "false",
|
||
"created": "1450742400",
|
||
"excerpt": "How to programmatically load, render and embed an entityform in Drupal 7.",
|
||
"body": "<p>I recently had my first experience using the <a href=\"https:\/\/www.drupal.org\/project\/entityform\">Entityform module<\/a> 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 <code>drupal_get_form()<\/code> function and render it, but this didn\u2019t work.<\/p>\n\n<p>Here are the steps that I took to be able to load, render and embed the form.<\/p>\n\n<h2 id=\"loading-the-form\">Loading the Form<\/h2>\n\n<p>The first thing that I needed to do to render the form was to load an empty instance of the entityform using <code>entityform_empty_load()<\/code>. In this example, <code>newsletter<\/code> is the name of my form type.<\/p>\n\n<pre><code class=\"language-php\">$form = entityform_empty_load('newsletter');\n<\/code><\/pre>\n\n<p>This returns an instance of a relevant <code>Entityform<\/code> object.<\/p>\n\n<h2 id=\"rendering-the-form\">Rendering the Form<\/h2>\n\n<p>The next step was to be able to render the form. I did this using the <code>entity_form_wrapper()<\/code> function.<\/p>\n\n<p>As this function is within the <code>entityform.admin.inc<\/code> file and not autoloaded by Drupal, I needed to include it using <code>module_load_include()<\/code> so that the function was available.<\/p>\n\n<pre><code class=\"language-php\">module_load_include('inc', 'entityform', 'entityform.admin');\n\n$output = entityform_form_wrapper($form, 'submit', 'embedded'),\n<\/code><\/pre>\n\n<p>The first argument is the <code>Entityform<\/code> object that was created in the previous step (I\u2019ve <a href=\"https:\/\/www.drupal.org\/node\/2639584\">submitted a patch<\/a> to type hint this within entityform so that it\u2019s clearer what is expected), which is required.<\/p>\n\n<p>The other two arguments are optional. The second argument is the mode (<code>submit<\/code> is the default value), and the last is the form context. <code>page<\/code> is the default value, for use on the submit page, however I changed this to <code>embedded<\/code>.<\/p>\n\n<p>I could then pass this result into my theme function to render it successfully within the relevant template file.<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/www.drupal.org\/project\/entityform\">The entityform module<\/a><\/li>\n<li><a href=\"https:\/\/www.drupal.org\/node\/2639584\">My issue and patch to add the type hint to the entityform_form_wrapper function<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"entityform"
|
||
]
|
||
}, {
|
||
"title": "Automating Sculpin Builds with Jenkins CI",
|
||
"path": "/articles/automating-sculpin-jenkins",
|
||
"is_draft": "false",
|
||
"created": "1437436800",
|
||
"excerpt": "How to use Jenkins to automate building Sculpin websites.",
|
||
"body": "<p>As part of re-building this site with <a href=\"http:\/\/sculpin.io\">Sculpin<\/a>, I wanted to automate the deployments, as in I wouldn't need to run a script like <a href=\"https:\/\/raw.githubusercontent.com\/sculpin\/sculpin-blog-skeleton\/master\/publish.sh\">publish.sh<\/a> 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.<\/p>\n\n<p>I'd started using <a href=\"http:\/\/jenkins-ci.org\">Jenkins CI<\/a> 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.<\/p>\n\n<h2 id=\"installing-jenkins-and-sculpin\">Installing Jenkins and Sculpin<\/h2>\n\n<p>If you don\u2019t already have Jenkins installed and configured, I'd suggest using <a href=\"http:\/\/jeffgeerling.com\/\">Jeff Geerling<\/a> (aka geerlingguy)'s <a href=\"https:\/\/galaxy.ansible.com\/list#\/roles\/440\">Ansible role for Jenkins CI<\/a>.<\/p>\n\n<p>I've also released an <a href=\"https:\/\/galaxy.ansible.com\/list#\/roles\/4063\">Ansible role for Sculpin<\/a> that installs the executable so that the Jenkins server can run Sculpin commands.<\/p>\n\n<h2 id=\"triggering-a-build-from-a-git-commit\">Triggering a Build from a Git Commit<\/h2>\n\n<p>I created a new Jenkins item for this task, and restricted where it could be run to <code>master<\/code> (i.e. the Jenkins server rather than any of the nodes).<\/p>\n\n<h3 id=\"polling-from-git\">Polling from Git<\/h3>\n\n<p>I entered the url to the <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\">GitHub repo<\/a> into the <strong>Source Code Management<\/strong> section (the Git option <em>may<\/em> have been added by the <a href=\"https:\/\/wiki.jenkins-ci.org\/display\/JENKINS\/Git+Plugin\">Git plugin<\/a> that I have installed).<\/p>\n\n<p>As we don\u2019t need any write access back to the repo, using the HTTP URL rather than the SSH one was fine, and I didn\u2019t need to provide any additional credentials.<\/p>\n\n<p>Also, as I knew that I\u2019d be working a lot with feature branches, I entered <code>*\/master<\/code> as the only branch to build. This meant that pushing changes or making edits on any other branches would not trigger a build.<\/p>\n\n<p><img src=\"\/images\/blog\/oliverdavies-uk-jenkins-git-repo.png\" alt=\"Defining the Git repository in Jenkins\" \/><\/p>\n\n<p>I also checked the <strong>Poll SCM<\/strong> 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 <code>* * * * *<\/code> so that Jenkins would poll each minute, knowing that I could make this less frequent if needed.<\/p>\n\n<p>This now that Jenkins would be checking for any updates to the repo each minute, and could execute tasks if needed.<\/p>\n\n<h3 id=\"building-and-deploying\">Building and Deploying<\/h3>\n\n<p>Within the <strong>Builds<\/strong> section of the item, I added an <em>Execute Shell<\/em> step, where I could enter a command to execute. Here, I pasted a modified version of the original publish.sh script.<\/p>\n\n<pre><code class=\"language-bash\">#!\/bin\/bash\n\nset -uex\n\nsculpin generate --env=prod --quiet\nif [ $? -ne 0 ]; then echo \"Could not generate the site\"; exit 1; fi\n\nrsync -avze 'ssh' --delete output_prod\/ prodwww2:\/var\/www\/html\/oliverdavies.uk\/htdocs\nif [ $? -ne 0 ]; then echo \"Could not publish the site\"; exit 1; fi\n<\/code><\/pre>\n\n<p>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, <code>prodwww2<\/code> is a Jenkins node (this alias is configured in <code>\/var\/lib\/jenkins\/.ssh\/config<\/code>), and <code>\/var\/www\/html\/oliverdavies.uk\/htdocs<\/code> is the directory from where my site is served.<\/p>\n\n<h2 id=\"building-periodically\">Building Periodically<\/h2>\n\n<p>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.<\/p>\n\n<p>The YAML front matter:<\/p>\n\n<pre><code class=\"language-yaml\">---\n...\ntalks:\n - title: Test Drive Twig with Sculpin\n location: DrupalCamp North\n---\n<\/code><\/pre>\n\n<p>The Twig layout:<\/p>\n\n<pre><code class=\"language-twig\">{% for talk in talks|reverse if talk.date >= now %}\n {# Upcoming talks #}\n{% endfor %}\n\n{% for talk in talks if talk.date < now %}\n {# Previous talks #}\n{% endfor%}\n<\/code><\/pre>\n\n<p>I also didn\u2019t 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.<\/p>\n\n<p>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 <strong>Build periodically<\/strong> option that I could use within the same item, leaving it intact and not having to make amends.<\/p>\n\n<p>I set this to <code>@daily<\/code> (the same <code>H H * * *<\/code> - <code>H<\/code> is a Jenkins thing), so that the build would be triggered automatically each day without a commit, and deploy any updates to the site.<\/p>\n\n<p><img src=\"\/images\/blog\/oliverdavies-uk-jenkins-git-timer.png\" alt=\"Setting Jenkins to periodically build a new version of the site.\" \/><\/p>\n\n<h2 id=\"next-steps\">Next Steps<\/h2>\n\n<p>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\u2019ll end up creating a separate <code>sculpin_build<\/code> item that\u2019s decoupled from the site that it\u2019s building, and instead passing variables such as environment, server name and docroot path as parameters in a parameterized build.<\/p>\n\n<p>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\u2019d 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.<\/p>\n\n<h2 id=\"update\">Update<\/h2>\n\n<p>Since publishing this post, I've added some more items to the original build script.<\/p>\n\n<h3 id=\"updating-composer\">Updating Composer<\/h3>\n\n<pre><code class=\"language-bash\">if [ -f composer.json ]; then\n \/usr\/local\/bin\/composer install\nfi\n<\/code><\/pre>\n\n<p>Updates project dependencies via <a href=\"https:\/\/getcomposer.org\/doc\/00-intro.md#introduction\">Composer<\/a> if composer.json exists.<\/p>\n\n<h3 id=\"updating-sculpin-dependencies\">Updating Sculpin Dependencies<\/h3>\n\n<pre><code class=\"language-bash\">if [ -f sculpin.json ]; then\n sculpin install\nfi\n<\/code><\/pre>\n\n<p>Runs <code>sculpin install<\/code> on each build if the sculpin.json file exists, to ensure that the required custom bundles and dependencies are installed.<\/p>\n\n<h3 id=\"managing-redirects\">Managing Redirects<\/h3>\n\n<pre><code class=\"language-bash\">if [ -f scripts\/redirects.php ]; then\n \/usr\/bin\/php scripts\/redirects.php\nfi\n<\/code><\/pre>\n\n<p>I've been working on a <code>redirects.php<\/code> script that generates redirects from a .csv file, after seeing similar things in the <a href=\"https:\/\/github.com\/pantheon-systems\/documentation\">Pantheon Documentation<\/a> and <a href=\"https:\/\/github.com\/thatpodcast\/thatpodcast.io\">That Podcast<\/a> repositories. This checks if that file exists, and if so, runs it and generates the source file containing each redirect.<\/p>\n",
|
||
"tags": ["sculpin"
|
||
,"jenkins"
|
||
]
|
||
}, {
|
||
"title": "Sculpin and Twig Resources",
|
||
"path": "/articles/sculpin-twig-resources",
|
||
"is_draft": "false",
|
||
"created": "1437264000",
|
||
"excerpt": "A list of resources that I compiled whilst preparing for my Sculpin and Twig talk at DrupalCamp North.",
|
||
"body": "<p>Here\u2019s a list of resources that I compiled whilst preparing for my <a href=\"http:\/\/drupalcampnorth.org\/session\/test-drive-twig-sculpin\">Sculpin and Twig talk<\/a> at <a href=\"http:\/\/drupalcampnorth.org\">DrupalCamp North<\/a>.<\/p>\n\n<h2 id=\"general-information\">General Information<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/sculpin.io\">https:\/\/sculpin.io<\/a><\/li>\n<li><a href=\"https:\/\/sculpin.io\/getstarted\/\">https:\/\/sculpin.io\/getstarted\/<\/a><\/li>\n<\/ul>\n\n<h2 id=\"where-to-get-sculpin\">Where to Get Sculpin<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/sculpin.io\/download\/\">https:\/\/sculpin.io\/download\/<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/sculpin\/sculpin\">https:\/\/github.com\/sculpin\/sculpin<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/sculpin\/sculpin-blog-skeleton\">https:\/\/github.com\/sculpin\/sculpin-blog-skeleton<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/opdavies\/sculpin-minimal\">https:\/\/github.com\/opdavies\/sculpin-minimal<\/a><\/li>\n<\/ul>\n\n<h2 id=\"source-code-examples\">Source Code Examples<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/sculpin.io\/community\/\">https:\/\/sculpin.io\/community\/<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\">https:\/\/github.com\/opdavies\/oliverdavies.uk<\/a> - the source repository for this site.<\/li>\n<li><a href=\"https:\/\/github.com\/simensen\/beau.io\">https:\/\/github.com\/simensen\/beau.io<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/simensen\/srcmvn.com\">https:\/\/github.com\/simensen\/srcmvn.com<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/thatpodcast\/thatpodcast.io\">https:\/\/github.com\/thatpodcast\/thatpodcast.io<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/MidwestPHP\/mwphp15\">https:\/\/github.com\/MidwestPHP\/mwphp15<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dev-human\/dev-human\">https:\/\/github.com\/dev-human\/dev-human<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/pantheon-systems\/documentation\">https:\/\/github.com\/pantheon-systems\/documentation<\/a><\/li>\n<li>Google for \"<code>sculpin_site.yml site:github.com<\/code>\" for more examples.<\/li>\n<\/ul>\n\n<h2 id=\"videos\">Videos<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/bit.ly\/sculpin-videos\">http:\/\/bit.ly\/sculpin-videos<\/a> - a YouTube playlist of Sculpin videos.<\/li>\n<\/ul>\n\n<h2 id=\"twig\">Twig<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/twig.sensiolabs.org\/\">http:\/\/twig.sensiolabs.org\/<\/a><\/li>\n<li><a href=\"http:\/\/twig.sensiolabs.org\/doc\/templates.html\">http:\/\/twig.sensiolabs.org\/doc\/templates.html<\/a> - variables, filters, functions, template inheritance, expressions etc.<\/li>\n<li>Go to http:\/\/twig.sensiolabs.org\/{foo} to search for a tag, filter, test or function.<\/li>\n<\/ul>\n",
|
||
"tags": ["sculpin"
|
||
,"drupalcamp"
|
||
,"drupalcamp-north"
|
||
,"twig"
|
||
]
|
||
}, {
|
||
"title": "Updating Forked Repositories on GitHub",
|
||
"path": "/articles/updating-forked-github-repos",
|
||
"is_draft": "false",
|
||
"created": "1434585600",
|
||
"excerpt": "I just had to update a repo that I forked on GitHub. This is how I did it. Did I do it the correct way?",
|
||
"body": "<p>I just had to update a repo that I forked on GitHub. This is how I did it. Did I do it the correct way?<\/p>\n\n<h2 id=\"sculpin\">Sculpin<\/h2>\n\n<p>People may or may not know, but this site runs on <a href=\"https:\/\/sculpin.io\/\">Sculpin<\/a>, a PHP based static site generator (this may be the first time that I've mentioned it on this site). The source code is hosted on <a href=\"https:\/\/github.com\/opdavies\/oliverdavies.uk\">GitHub<\/a>, and I've listed the site on the <a href=\"https:\/\/sculpin.io\/community\/\">Community page<\/a> on the Sculpin website.<\/p>\n\n<p>To get it there, I forked the <a href=\"https:\/\/github.com\/sculpin\/sculpin.io\">main sculpin.io repository<\/a> so that I had <a href=\"https:\/\/github.com\/opdavies\/sculpin.io\">my own copy<\/a>, created a branch, made my additions and submitted a pull request. Easy enough!<\/p>\n\n<h2 id=\"new-domain\">New Domain<\/h2>\n\n<p>In the last week or so, I've changed this site URL from .co.uk to just .uk, and also updated the GitHub repo URL to match, so I wanted to update the Community page to use the correct URL.<\/p>\n\n<p>There had been commits to the main repo since my pull request was merged, I didn't want to delete my repo and fork again, and making any changes against and old codebase isn't best practice, so I wanted to merge the latest changes into my forked repo before I did anything else - just to check that I didn't break anything!<\/p>\n\n<h2 id=\"updating-my-local-repo\">Updating my Local Repo<\/h2>\n\n<p>I had a quick look for a <em>Update my fork<\/em> button or something, but couldn't see one to I added the main repository as an additional remote called <code>upstream<\/code> and fetched the changes.<\/p>\n\n<pre><code class=\"language-bash\">$ git remote add upstream https:\/\/github.com\/sculpin\/sculpin.io.git\n\n$ git fetch upstream\nremote: Counting objects: 33, done.\nremote: Total 33 (delta 6), reused 6 (delta 6), pack-reused 27\nUnpacking objects: 100% (33\/33), done.\nFrom https:\/\/github.com\/sculpin\/sculpin.io\n* [new branch] master -> upstream\/master\n* [new branch] pr\/4 -> upstream\/pr\/4\n<\/code><\/pre>\n\n<p>Now my local site knows about the upstream repo, and I could rebase the changes (<code>git pull upstream master<\/code> should have worked too) and push them back to origin.<\/p>\n\n<pre><code class=\"language-bash\">$ git rebase upstream\/master\nFirst, rewinding head to replay your work on top of it...\n...\nFast-forwarded master to upstream\/master.\n\n$ git push origin master\n<\/code><\/pre>\n\n<p>This seems to have worked OK - the commits are still authored by the correct people and at the correct date and time - and I went ahead and created a new feature branch and pull request based on that master branch.<\/p>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/forked-github-repo-commits.png\" alt=\"The commits on my master branch after rebasing\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The commits on my forked master branch after rebasing and pushing. All good!\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/my-commit-to-the-rebased-branch.png\" alt=\"The new feature branch with my additional commit\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The new feature branch with the new commit.\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<h2 id=\"is-there-a-better-way%3F\">Is There a Better Way?<\/h2>\n\n<p>Did I miss something? Is there a recommended and\/or better way to update your forked repos, maybe through the UI? Please <a href=\"https:\/\/twitter.com\/?status=Rebasing GitHub Forks: @opdavies\">send me a tweet<\/a> with any comments.<\/p>\n\n<h2 id=\"up\">Up<\/h2>\n\n<p><strong>December 2015:<\/strong> I\u2019ve found that PhpStorm has an option available to rebase a fork from within the IDE. This is within the <em>VCS<\/em> > <em>Git<\/em> menu.<\/p>\n\n<p>I believe that it will use an existing \"upstream\" remote if it exists, otherwise it will add one automatically for you, linking to the repository that you forked from.<\/p>\n\n<p>Once you\u2019ve completed the rebase, you can then push your updated branch either from the terminal, or using the <em>Push<\/em> command from the same menu.<\/p>\n\n<p><img src=\"\/images\/blog\/github-fork-rebase-phpstorm.png\" alt=\"Rebasing a forked repository in PhpStorm using the VCS menu.\" \/><\/p>\n\n<p>It would be great to see something similar added to <a href=\"https:\/\/hub.github.com\">hub<\/a> too (I\u2019ve created <a href=\"https:\/\/github.com\/github\/hub\/issues\/1047\">an issue<\/a>)!<\/p>\n\n<h2 id=\"resources\">Resources<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/blog.jetbrains.com\/idea\/2011\/02\/advanced-github-integration-rebase-my-github-fork\/\">PhpStorm - Advanced GitHub Integration: Rebase My GitHub Fork (blog post)<\/a><\/li>\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=Twy-dhVgN4k\">Rebasing a GitHub fork inside PhpStorm (video)<\/a><\/li>\n<li><a href=\"https:\/\/hub.github.com\">hub<\/a> - makes Git better with GitHub<\/li>\n<\/ul>\n",
|
||
"tags": ["git"
|
||
,"github"
|
||
,"phpstorm"
|
||
,"sculpin"
|
||
]
|
||
}, {
|
||
"title": "How to Define a Minimum Drupal Core Version",
|
||
"path": "/articles/minimum-core-version",
|
||
"is_draft": "false",
|
||
"created": "1428019200",
|
||
"excerpt": "How to define a minimum Drupal core version for your module or theme.",
|
||
"body": "<p>This week, my first code patch was <a href=\"https:\/\/www.drupal.org\/node\/2394517#comment-9773143\">committed to Drupal core<\/a>. The patch adds the <code>user_has_role()<\/code> 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 <a href=\"https:\/\/www.drupal.org\/drupal-7.36-release-notes\">7.36<\/a>.<\/p>\n\n<p>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.<\/p>\n\n<p>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?<\/p>\n\n<h2 id=\"setting-dependencies\">Setting Dependencies<\/h2>\n\n<p>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.<\/p>\n\n<h3 id=\"adding-a-simple-dependency\">Adding a Simple Dependency<\/h3>\n\n<p>You can define a simple dependency for your module by adding a line this this to your project's .info file:<\/p>\n\n<pre><code class=\"language-bash\">dependencies[] = views\n<\/code><\/pre>\n\n<p>This would make your module dependant on having the <a href=\"https:\/\/www.drupal.org\/project\/views\">Views<\/a> module present and enabled, which you'd need if you were including views as part of your module, for example.<\/p>\n\n<h3 id=\"adding-a-complex-dependency\">Adding a Complex Dependency<\/h3>\n\n<p>In the previous example, our module would enable if <em>any<\/em> 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:<\/p>\n\n<pre><code class=\"language-bash\">dependencies[] = modulename (major.minor)\n<\/code><\/pre>\n\n<p>This can be a for a specific module release or a branch name:<\/p>\n\n<pre><code class=\"language-bash\">dependencies[] = modulename (1.0)\ndependencies[] = modulename (1.x)\n<\/code><\/pre>\n\n<p>We can also use the following as part of the field for extra granularity:<\/p>\n\n<ul>\n<li>= or == equals (this is the default)<\/li>\n<li>> greater than<\/li>\n<li>< lesser than<\/li>\n<li>>= greater than or equal to<\/li>\n<li><= lesser than or equal to<\/li>\n<li>!= not equal to<\/li>\n<\/ul>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-ini\">dependencies[] = system (>=7.36)\n<\/code><\/pre>\n\n<p>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 <code>user_has_role()<\/code> when it is called.<\/p>\n\n<p><img src=\"\/images\/blog\/minimum-drupal-version-d7.png\" alt=\"A screenshot of the modules page showing System as a dependency for a custom module.\" \/><\/p>\n\n<h2 id=\"external-links\">External Links<\/h2>\n\n<ul>\n<li><a href=\"https:\/\/www.drupal.org\/node\/542202#dependencies\">Writing module .info files (Drupal 7.x)<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
]
|
||
}, {
|
||
"title": "2014",
|
||
"path": "/articles/2014",
|
||
"is_draft": "false",
|
||
"created": "1426882440",
|
||
"excerpt": "A look back at 2014.",
|
||
"body": "<p>A lot happened in 2014. Here are some of the main things that I'd like to highlight.<\/p>\n\n<h2 id=\"joined-the-drupal-association\">Joined the Drupal Association<\/h2>\n\n<p>This was the main thing for me this year, in May I left <a href=\"http:\/\/precedent.com\">Precedent<\/a> and joined the <a href=\"https:\/\/assoc.drupal.org\">Drupal Association<\/a>. I work on the Engineering team, focused mainly on <a href=\"https:\/\/www.drupal.org\">Drupal.org<\/a> but I've also done some theming work on the DrupalCon <a href=\"http:\/\/amsterdam2014.drupal.org\">Amsterdam<\/a> and <a href=\"http:\/\/latinamerica2015.drupal.org\">Latin America<\/a> websites, and some pre-launch work on <a href=\"https:\/\/jobs.drupal.org\">Drupal Jobs<\/a>.<\/p>\n\n<p>Some of the tasks that I've worked on so far are:<\/p>\n\n<ul>\n<li>Fixing remaining issues from the Drupal.org Drupal 7 upgrade.<\/li>\n<li>Improving pages for <a href=\"https:\/\/www.drupal.org\/supporters\/partners\">Supporting Partners<\/a>, <a href=\"https:\/\/www.drupal.org\/supporters\/technology\">Technology Supporters<\/a> and <a href=\"https:\/\/www.drupal.org\/supporters\/hosting\">Hosting Partners<\/a>. These previously were manually updated pages using HTML tables, which are now dynamic pages built with <a href=\"https:\/\/www.drupal.org\/project\/views\">Views<\/a> using organisation nodes.<\/li>\n<li>Configuring human-readable paths for user profiles using <a href=\"https:\/\/www.drupal.org\/project\/pathauto\">Pathauto<\/a>. Only a small change, but made a big difference to end-users.<\/li>\n<li>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.<\/li>\n<\/ul>\n\n<p>I've started keeping a list of tasks that I've been involved with on my <a href=\"\/work\/\">Work<\/a> page, and will be adding more things as I work on them.<\/p>\n\n<h3 id=\"portland\">Portland<\/h3>\n\n<p>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.<\/p>\n\n<h2 id=\"my-first-drupalcamp\">My First DrupalCamp<\/h2>\n\n<p>In February, I attended <a href=\"http:\/\/2014.drupalcamplondon.co.uk\">DrupalCamp London<\/a>. 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 <a href=\"\/blog\/what-git-flow\/\">Git Flow<\/a> - a workflow for managing your Git projects.<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<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>\n\n<p>— Greg Franklin (@gfranklin) <a href=\"https:\/\/twitter.com\/gfranklin\/statuses\/440104311276969984\">March 2, 2014<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p>I was also able to do a little bit of sprinting whilst I was there, reviewing other people's modules and patches.<\/p>\n\n<p>Attending this and <a href=\"https:\/\/prague2013.drupal.org\">DrupalCon Prague<\/a> 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.<\/p>\n\n<h2 id=\"drupalcon-amsterdam\">DrupalCon Amsterdam<\/h2>\n\n<p>I was also able to travel to Holland and attend <a href=\"https:\/\/amsterdam2014.drupal.org\">DrupalCon Amsterdam<\/a> along with other members of Association staff.<\/p>\n\n<h2 id=\"drupalcamp-bristol\">DrupalCamp Bristol<\/h2>\n\n<p>In October, we started planning for <a href=\"http:\/\/www.drupalcampbristol.co.uk\">DrupalCamp Bristol<\/a>. I'm one of the founding Committee members,<\/p>\n",
|
||
"tags": ["drupal-association"
|
||
,"drupalcamp-london"
|
||
,"personal"
|
||
]
|
||
}, {
|
||
"title": "Configuring the Reroute Email Module",
|
||
"path": "/articles/configuring-the-reroute-email-module",
|
||
"is_draft": "true",
|
||
"created": "1419206400",
|
||
"excerpt": "How to configure the Reroute Email module, to prevent sending emails to real users from your pre-production sites!",
|
||
"body": "<p><a href=\"https:\/\/www.drupal.org\/project\/reroute_email\">Reroute Email<\/a> module uses <code>hook_mail_alter()<\/code> 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.<\/p>\n\n<blockquote>\n <p>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.<\/p>\n<\/blockquote>\n\n<p>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.<\/p>\n\n<p>The first thing that we need to do is to enable rerouting. Without doing this, nothing will happen.<\/p>\n\n<pre><code class=\"language-php\">$conf['reroute_email_enable'] = TRUE;\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\">$conf['reroute_email_enable_message'] = TRUE;\n<\/code><\/pre>\n\n<p>The last setting is the email address to use. If you're entering a single address, you can add it as a simple string.<\/p>\n\n<pre><code class=\"language-php\">$conf['reroute_email_address'] = 'person1@example.com';\n<\/code><\/pre>\n\n<p>In this example, all emails from the site will be rerouted to person1@example.com.<\/p>\n\n<p>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 <code>implode()<\/code> function.<\/p>\n\n<pre><code class=\"language-php\">$conf['reroute_email_address'] = implode(';', array(\n 'person1@example.com',\n 'person2@example.com',\n 'person3@example.com',\n));\n<\/code><\/pre>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-6"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"email"
|
||
,"drafts"
|
||
]
|
||
}, {
|
||
"title": "Include a Local Drupal Settings file for Environment Configuration and Overrides",
|
||
"path": "/articles/include-local-drupal-settings-file-environment-configuration-and-overrides",
|
||
"is_draft": "false",
|
||
"created": "1419033600",
|
||
"excerpt": "How to create and include a local settings file to define and override environment-specific variables.",
|
||
"body": "<p>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.<\/p>\n\n<p>At the bottom of settings.php, add the following code:<\/p>\n\n<pre><code class=\"language-php\">$local_settings = __DIR__ . '\/settings.local.php';\nif (file_exists($local_settings)) {\n include $local_settings;\n}\n<\/code><\/pre>\n\n<p>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 <a href=\"http:\/\/cgit.drupalcode.org\/drupal\/tree\/sites\/default\/default.settings.php#n621\">default.settings.php<\/a> file.<\/p>\n\n<p>Environment specific settings like <code>$databases<\/code> and <code>$base_url<\/code> can be placed within the local settings file. Other settings like <code>$conf['locale_custom_strings_en']<\/code> (string overrides) and <code>$conf['allow_authorize_operations']<\/code> that would apply to all environments can still be placed in settings.php.<\/p>\n\n<p>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 <code>--force<\/code> option when adding the file which overrides the ignore file:<\/p>\n\n<pre><code class=\"language-bash\">git add --force sites\/default\/settings.php\n<\/code><\/pre>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-bash\"># Ignore configuration files that may contain sensitive information.\nsites\/*\/settings.local*.php\n\n# Ignore paths that contain user-generated content.\nsites\/*\/files\nsites\/*\/private\n<\/code><\/pre>\n\n<p>This will allow for settings.php to be added to Git and committed, but not settings.local.php.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-6"
|
||
,"drupal-7"
|
||
,"drupal-8"
|
||
,"drupal-planet"
|
||
,"settings.php"
|
||
]
|
||
}, {
|
||
"title": "Include environment-specific settings files on Pantheon",
|
||
"path": "/articles/pantheon-settings-files",
|
||
"is_draft": "false",
|
||
"created": "1417046400",
|
||
"excerpt": "How to load a different settings file per environment on Pantheon.",
|
||
"body": "<p>I was recently doing some work on a site hosted on <a href=\"http:\/\/getpantheon.com\">Pantheon<\/a> and came across an issue, for which part of the suggested fix was to ensure that the <code>$base_url<\/code> variable was explicitly defined within settings.php (this is also best practice on all Drupal sites).<\/p>\n\n<p>The way that was recommended was by using a <code>switch()<\/code> function based on Pantheon's environment variable. For example:<\/p>\n\n<pre><code class=\"language-php\">switch ($_SERVER['PANTHEON_ENVIRONMENT']) {\n case 'dev':\n \/\/ Development environment.\n $base_url = 'dev-my-site.gotpantheon.com';\n break;\n\n\n case 'test':\n \/\/ Testing environment.\n $base_url = 'test-my-site.gotpantheon.com';\n break;\n\n\n case 'live':\n \/\/ Production environment.\n $base_url = 'live-my-site.gotpantheon.com';\n break;\n}\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>My alternative solution to this is to include an environment-specific settings file.<\/p>\n\n<p>To do this, add the following code to the bottom of settings.php:<\/p>\n\n<pre><code class=\"language-php\">if (isset($_SERVER['PANTHEON_ENVIRONMENT'])) {\n if ($_SERVER['PANTHEON_ENVIRONMENT'] != 'live') {\n \/\/ You can still add things here, for example to apply to all sites apart\n \/\/ from production. Mail reroutes, caching settings etc.\n }\n\n \/\/ Include an environment-specific settings file, for example\n \/\/ settings.dev.php, if one exists.\n $environment_settings = __DIR__ . '\/settings.' . $_SERVER['PANTHEON_ENVIRONMENT'] . '.php';\n if (file_exists($environment_settings)) {\n include $environment_settings;\n }\n}\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>Below this, I also include a <a href=\"\/blog\/include-local-drupal-settings-file-environment-configuration-and-overrides\/\">similar piece of code<\/a> to include a settings.local.php file. The settings.php file then gets committed into the <a href=\"http:\/\/git-scm.com\">Git<\/a> repository.<\/p>\n\n<p>Within the sites\/default directory, I also include an example file (example.settings.env.php) for reference. This is duplicated, renamed and populated accordingly.<\/p>\n\n<pre><code class=\"language-php\"><?php\n\n\/**\n * This is a specific settings file, just for the x environment. Any settings\n * defined here will be included after those in settings.php.\n *\n * If you have also added a settings.local.php file, that will override any\n * settings stored here.\n *\n * No database credentials should be stored in this file as these are included\n * automatically by Pantheon.\n *\/\n\n$base_url = '';\n<\/code><\/pre>\n\n<p>The environment specific files are also committed into Git and pushed to Pantheon, and are then included automatically on each environment.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"pantheon"
|
||
,"settings.php"
|
||
]
|
||
}, {
|
||
"title": "Using Remote Files when Developing Locally with Stage File Proxy Module",
|
||
"path": "/articles/using-remote-files-when-developing-locally-stage-file-proxy-module",
|
||
"is_draft": "false",
|
||
"created": "1416441600",
|
||
"excerpt": "How to install and configure the Stage File Proxy module to serve remote images on your local Drupal site.",
|
||
"body": "<p>How to install and configure the <a href=\"https:\/\/www.drupal.org\/project\/stage_file_proxy\">Stage File Proxy<\/a> module to serve remote images on your local Drupal site.<\/p>\n\n<p>As this module is only going to be needed on pre-production sites, it would be better to configure this within your settings.php or settings.local.php file. We do this using the <code>$conf<\/code> array which removes the need to configure the module through the UI and store the values in the database.<\/p>\n\n<pre><code class=\"language-php\">\/\/ File proxy to the live site.\n$conf['stage_file_proxy_origin'] = 'http:\/\/www.example.com';\n\n\/\/ Don't copy the files, just link to them.\n$conf['stage_file_proxy_hotlink'] = TRUE;\n\n\/\/ Image style images are the wrong size otherwise.\n$conf['stage_file_proxy_use_imagecache_root'] = FALSE;\n<\/code><\/pre>\n\n<p>If the origin site is not publicly accessible yet, maybe it's a pre-live or staging site, and protected with a basic access authentication, you can include the username and password within the origin URL.<\/p>\n\n<pre><code class=\"language-php\">$conf['stage_file_proxy_origin'] = 'http:\/\/user:password@prelive.example.com';\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"servers"
|
||
]
|
||
}, {
|
||
"title": "Include CSS Fonts by Using a SASS each Loop",
|
||
"path": "/articles/include-css-fonts-using-sass-each-loop",
|
||
"is_draft": "false",
|
||
"created": "1416268800",
|
||
"excerpt": "How to use an SASS each loop to easily add multiple fonts to your CSS.",
|
||
"body": "<p>How to use an @each loop in SASS to quickly include multiple font files within your stylesheet.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">.\n\u251c\u2500\u2500 FuturaBold\n\u2502 \u251c\u2500\u2500 FuturaBold.eot\n\u2502 \u251c\u2500\u2500 FuturaBold.svg\n\u2502 \u251c\u2500\u2500 FuturaBold.ttf\n\u2502 \u2514\u2500\u2500 FuturaBold.woff\n\u251c\u2500\u2500 FuturaBoldItalic\n\u2502 \u251c\u2500\u2500 FuturaBoldItalic.eot\n\u2502 \u251c\u2500\u2500 FuturaBoldItalic.svg\n\u2502 \u251c\u2500\u2500 FuturaBoldItalic.ttf\n\u2502 \u2514\u2500\u2500 FuturaBoldItalic.woff\n\u251c\u2500\u2500 FuturaBook\n\u2502 \u251c\u2500\u2500 FuturaBook.eot\n\u2502 \u251c\u2500\u2500 FuturaBook.svg\n\u2502 \u251c\u2500\u2500 FuturaBook.ttf\n\u2502 \u2514\u2500\u2500 FuturaBook.woff\n\u251c\u2500\u2500 FuturaItalic\n\u2502 \u251c\u2500\u2500 FuturaItalic.eot\n\u2502 \u251c\u2500\u2500 FuturaItalic.svg\n\u2502 \u251c\u2500\u2500 FuturaItalic.ttf\n\u2502 \u2514\u2500\u2500 FuturaItalic.woff\n<\/code><\/pre>\n\n<p>Within your SASS file, start an <code>@each<\/code> loop, listing the names of the fonts. In the same way as PHP's <code>foreach<\/code> loop, each font name will get looped through using the <code>$family<\/code> variable and then compiled into CSS.<\/p>\n\n<pre><code class=\"language-scss\">@each $family in FuturaBook, FuturaBold, FuturaBoldItalic, FuturaItalic {\n @font-face {\n font-family: #{$family};\n src: url('..\/fonts\/#{$family}\/#{$family}.eot');\n src: url('..\/fonts\/#{$family}\/#{$family}.eot?#iefix') format('embedded-opentype'),\n url('..\/fonts\/#{$family}\/#{$family}.woff') format('woff'),\n url('..\/fonts\/#{$family}\/#{$family}.ttf') format('truetype'),\n url('..\/fonts\/#{$family}\/#{$family}.svg##{$family}') format('svg');\n font-weight: normal;\n font-style: normal;\n }\n}\n<\/code><\/pre>\n\n<p>When the CSS has been compiled, you can then use in your CSS in the standard way.<\/p>\n\n<pre><code class=\"language-scss\">font-family: \"FuturaBook\";\n<\/code><\/pre>\n",
|
||
"tags": ["compass"
|
||
,"drupal-planet"
|
||
,"fonts"
|
||
,"sass"
|
||
]
|
||
}, {
|
||
"title": "Updating Features and Adding Components Using Drush",
|
||
"path": "/articles/updating-features-and-adding-components-using-drush",
|
||
"is_draft": "false",
|
||
"created": "1413849600",
|
||
"excerpt": "How to update features on the command line using Drush.",
|
||
"body": "<p>If you use the <a href=\"http:\/\/drupal.org\/project\/features\">Features module<\/a> to manage your Drupal configuration, it can be time consuming to update features through the UI, especially if you are working on a remote server and need to keep downloading and uploading files.<\/p>\n\n<p>If you re-create a feature through the UI, you'll be prompted to download a new archive of the feature in its entirety onto your local computer. You could either commit this into a local repository and then pull it remotely, or use a tool such as SCP to upload the archive onto the server and commit it from there. You can simplify this process by using <a href=\"http:\/\/drush.org\">Drush<\/a>.<\/p>\n\n<h2 id=\"finding-components\">Finding Components<\/h2>\n\n<p>To search for a component, use the <code>drush features-components<\/code> command. This will display a list of all components on the site. As we're only interested in components that haven't been exported yet, add the <code>--not-exported<\/code> option to filter the results.<\/p>\n\n<p>To filter further, you can also use the <code>grep<\/code> command to filter the results. For example, <code>drush features-components --not-exported field_base | grep foo<\/code>, would only return non-exported field bases containing the word \"foo\".<\/p>\n\n<p>The result is a source and a component, separated by a colon. For example, <code>field_base:field_foo<\/code>.<\/p>\n\n<h2 id=\"exporting-the-feature\">Exporting the Feature<\/h2>\n\n<p>Once you have a list of the components that you need to add, you can export the feature. This is done using the <code>drush features-export<\/code> command, along with the feature name and the component names.<\/p>\n\n<p>For example:<\/p>\n\n<pre><code class=\"language-bash\">$ drush features-export -y myfeature field_base:field_foo field_instance:user-field_foo\n<\/code><\/pre>\n\n<p>In this example, the base for field_boo and it's instance on the user object is being added to the \"myfeature\" feature.<\/p>\n\n<p>If you are updating an existing feature, you'll get a message informing you that the module already exists and asking if you want to continue. This is fine, and is automatically accepted by including <code>-y<\/code> within the command. If a feature with the specified name doesn't exist, it will be created.<\/p>\n\n<p>If you're creating a new feature, you can define where the feature will be created using the <code>--destination<\/code> option.<\/p>\n\n<p>Once complete, you will see a confirmation message.<\/p>\n\n<blockquote>\n <p>Created module: my feature in sites\/default\/modules\/custom\/features\/myfeature<\/p>\n<\/blockquote>\n\n<h2 id=\"the-result\">The Result<\/h2>\n\n<p>Once finished, the feature is updated in it's original location, so there's no download of the feature and then needing to re-upload it. You can add and commit your changes into Git or continue with your standard workflow straight away.<\/p>\n\n<h2 id=\"useful-links\">Useful Links<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/www.drupal.org\/project\/features\">The Features project page on Drupal.org<\/a><\/li>\n<li><a href=\"http:\/\/www.drushcommands.com\/drush-6x\/features\/features-components\">The \"drush features-components\" command<\/a><\/li>\n<li><a href=\"http:\/\/www.drushcommands.com\/drush-6x\/features\/features-export\">The \"drush features-export\" command<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drush"
|
||
,"features"
|
||
]
|
||
}, {
|
||
"title": "How to fix Vagrant Loading the Wrong Virtual Machine",
|
||
"path": "/articles/fix-vagrant-loading-wrong-virtual-machine",
|
||
"is_draft": "false",
|
||
"created": "1412553600",
|
||
"excerpt": "Here are the steps that I took to fix Vagrant and point it back at the correct VM.",
|
||
"body": "<p>A few times recently, I've had instances where <a href=\"https:\/\/www.vagrantup.com\">Vagrant<\/a> 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.<\/p>\n\n<p>Here are the steps that I took to fix this and point Vagrant back at the correct VM.<\/p>\n\n<ol>\n<li>Stop the machine from running using the <code>$ vagrant halt<\/code> command.<\/li>\n<li>Use the <code>$ VBoxManage list vms<\/code> 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, <code>\"foo_default_1405481857614_74478\" {e492bfc3-cac2-4cde-a396-e81e37e421e2}<\/code>. The number within the curly brackets is the ID of the virtual machine.<\/li>\n<li>Within the .vagrant directory in your project (it is hidden by default), update the ID within the machines\/default\/virtualbox\/id file.<\/li>\n<li>Start the new VM with <code>$ vagrant up<\/code>.<\/li>\n<\/ol>\n",
|
||
"tags": ["vagrant"
|
||
,"virtualbox"
|
||
]
|
||
}, {
|
||
"title": "git format-patch is your Friend",
|
||
"path": "/articles/git-format-patch",
|
||
"is_draft": "false",
|
||
"created": "1400630400",
|
||
"excerpt": "An explanation of the \"git format-patch\" command, and how it could be used in Drupal's Git workflow.",
|
||
"body": "<p>An explanation of the \"git format-patch\" command, and how it could be used in Drupal's Git workflow.<\/p>\n\n<h2 id=\"the-problem\">The Problem<\/h2>\n\n<p>As an active contributor to the <a href=\"http:\/\/drupal.org\">Drupal<\/a> project, I spend a lot of time working with other peoples\u2019 modules and themes, and occassionally have to fix a bug or add some new functionality.<\/p>\n\n<p>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.<\/p>\n\n<p>There is an option that the maintainer can add to the end of their commit message.<\/p>\n\n<p>For example:<\/p>\n\n<pre><code class=\"language-bash\">--author=\"opdavies <opdavies@381388.no-reply.drupal.org>\"\n<\/code><\/pre>\n\n<p>This differs slightly different for each Drupal user, and the code can be found on their Drupal.org profile page.<\/p>\n\n<p>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\u2019ve made a commit to that project.<\/p>\n\n<p><img src=\"\/images\/blog\/git-format-patch.png\" alt=\"A screenshot of a commit that was authored by rli but committed by opdavies\" \/><\/p>\n\n<p>The problem is that some project maintainers either don\u2019t know about this option or occasionally forget to add it. <a href=\"http:\/\/dreditor.org\">Dreditor<\/a> 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).<\/p>\n\n<p>The <code>git format-patch<\/code> command seems to be the answer, and will be my preferred method for generating patch files in the future rather than <code>git diff<\/code>.<\/p>\n\n<h2 id=\"what-does-it-do-differently%3F\">What does it do Differently?<\/h2>\n\n<p>From the <a href=\"http:\/\/git-scm.com\/docs\/git-format-patch\">manual page<\/a>:<\/p>\n\n<blockquote>\n <p>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.<\/p>\n<\/blockquote>\n\n<p>Here is a section of a patch that I created for the <a href=\"http:\/\/drupal.org\/project\/metatag\">Metatag module<\/a> using <code>git format-patch<\/code>:<\/p>\n\n<pre><code class=\"language-bash\">From 80c8fa14de7f4a83c2e70367aab0aedcadf4f3b0 Mon Sep 17 00:00:00 2001\nFrom: Oliver Davies &lt;oliver@oliverdavies.co.uk&gt;\nSubject: [PATCH] Exclude comment entities when checking if this is the page,\n otherwise comment_fragment.module will break metatag\n---\n<\/code><\/pre>\n\n<p>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\u2019re interested in is the \u201cFrom\u201d value. This contains your name and email address from your <code>~\/.gitconfig<\/code> file and is used to author the patch automatically.<\/p>\n\n<p>Everything below this is the same as a standard patch file, the same as if was generated with <code>git diff<\/code>.<\/p>\n\n<p>The full patch file can be found at <a href=\"https:\/\/drupal.org\/files\/issues\/metatag-comment-fragment-conflict-2265447-4.patch\">https:\/\/drupal.org\/files\/issues\/metatag-comment-fragment-conflict-2265447-4.patch<\/a>.<\/p>\n\n<h2 id=\"the-process\">The Process<\/h2>\n\n<p>How did I create this patch? Here are the steps that I took:<\/p>\n\n<ol>\n<li>Clone the source repository using <code>$ git clone --branch 7.x-1.x http:\/\/git.drupal.org\/project\/metatag.git<\/code> and move into that directory.<\/li>\n<li>Create a branch for this patch using <code>$ git checkout -b 2265447-comment-fragment-conflict<\/code>.<\/li>\n<li>Add and commit any changes as normal.<\/li>\n<li>Generate the patch file using <code>$ git format-patch 7.x-1.x --stdout > metatag-comment-fragment-conflict-2265447-4.patch<\/code>.<\/li>\n<\/ol>\n\n<p><em>Note:<\/em> 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\u2019s version number. Also, commits should always be made against the development branch and not the stable release.<\/p>\n\n<p>By default, a separate patch file will be created for each commit that we\u2019ve made. This is overridden by the <code>--stdout<\/code> option which combines all of the patches into a single file. This is the recommended approach when uploading to Drupal.org.<\/p>\n\n<p>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.<\/p>\n\n<h2 id=\"committing-the-patch\">Committing the Patch<\/h2>\n\n<p>If you need to commit a patch that was created using <code>git format-patch<\/code>, the best command to do this with is the <code>git am<\/code> command.<\/p>\n\n<p>For example, within your repository, run:<\/p>\n\n<pre><code class=\"language-bash\">$ git am \/path\/to\/file\n$ git am ~\/Code\/metatag-comment-fragment-conflict-2265447-4.patch\n<\/code><\/pre>\n\n<p>You should end up with some output similar to the following:<\/p>\n\n<pre><code class=\"language-bash\">Applying: #2272799 Added supporters section\nApplying: #2272799 Added navigation tabs\nApplying: #2272799 Fixed indentation\nApplying: #2272799 Replaced URL\n<\/code><\/pre>\n\n<p>Each line is the commit message associated with that patch.<\/p>\n\n<p>Assuming that there are no errors, you can go ahead and push your updated code into your remote repository.<\/p>\n",
|
||
"tags": ["patches"
|
||
,"drupal"
|
||
,"drupal-planet"
|
||
,"git"
|
||
]
|
||
}, {
|
||
"title": "Thanks",
|
||
"path": "/articles/thanks",
|
||
"is_draft": "false",
|
||
"created": "1399334400",
|
||
"excerpt": "Thanks everyone for their comments about my move to the Drupal Association.",
|
||
"body": "<p>This is just a quick post to thank everyone for their comments and congratulations after my previous post about <a href=\"\/blog\/drupal-association\/\">joining the Drupal Association<\/a>. I\u2019m looking forward to my first day in the job tomorrow.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-association"
|
||
,"personal"
|
||
]
|
||
}, {
|
||
"title": "Drupal Association",
|
||
"path": "/articles/drupal-association",
|
||
"is_draft": "false",
|
||
"created": "1399075200",
|
||
"excerpt": "Next week, I'll be working for the Drupal Association.",
|
||
"body": "<p>Today was my last day working at <a href=\"http:\/\/www.precedent.com\">Precedent<\/a>. Next week, I'll be starting my <a href=\"https:\/\/assoc.drupal.org\/node\/18923\" title=\"Drupal.org Developer\">new job<\/a> at the <a href=\"http:\/\/assoc.drupal.org\">Drupal Association<\/a> working on Drupal's home - <a href=\"http:\/\/www.drupal.org\">Drupal.org<\/a>.<\/p>\n\n<p>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.<\/p>\n\n<p>I was also lucky enough to go to <a href=\"http:\/\/prague2013.drupal.org\">DrupalCon Prague<\/a> as well as <a href=\"http:\/\/2014.drupalcamplondon.co.uk\">DrupalCamp London<\/a>.<\/p>\n\n<p>I was able to <a href=\"https:\/\/drupal.org\/project\/eventsforce\">contribute some code<\/a> back into the community and encourage other team members to do the same.<\/p>\n\n<p>It was good to be able to introduce some new tools like <a href=\"http:\/\/www.vagrantup.com\">Vagrant<\/a>, <a href=\"http:\/\/www.puppetlabs.com\">Puppet<\/a>, <a href=\"http:\/\/www.sass-lang.com\">SASS<\/a> and <a href=\"http:\/\/www.compass-style.org\">Compass<\/a> into the team. I was pleased to introduce and champion the <a href=\"http:\/\/danielkummer.github.io\/git-flow-cheatsheet\" title=\"Git Flow Cheat Sheet\">Git Flow<\/a> branching model, which them became the standard approach for all Drupal projects, and hopefully soon all development projects.<\/p>\n\n<p>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!<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"personal"
|
||
]
|
||
}, {
|
||
"title": "DrupalCamp London: What is Git Flow?",
|
||
"path": "/articles/what-git-flow",
|
||
"is_draft": "false",
|
||
"created": "1393804800",
|
||
"excerpt": "Here are my slides from my \"What is Git Flow?\" session at DrupalCamp London.",
|
||
"body": "<p>Here are my slides from my \"What is Git Flow?\" session at <a href=\"http:\/\/2014.drupalcamplondon.co.uk\">DrupalCamp London<\/a>.<\/p>\n\n<p><div class=\"slides\">\n <noscript>**Please enable JavaScript to view slides.**<\/noscript>\n <script\n class=\"speakerdeck-embed\"\n data-id=\"\"\n data-ratio=\"1.29456384323641\"\n src=\"\/\/speakerdeck.com\/assets\/embed.js\"\n ><\/script>\n<\/div>\n<\/p>\n\n<h2 id=\"take-aways\">Take aways<\/h2>\n\n<p>The main take aways are:<\/p>\n\n<ul>\n<li>Git Flow adds various commands into Git to enhance its native functionality, which creates a branching model to separate your stable production code from your unstable development code.<\/li>\n<li>Never commit directly to the master branch - this is for production code only!<\/li>\n<li>You can commit directly to the develop branch, but this should be done sparingly.<\/li>\n<li>Use feature branches as much as possible - one per feature, user story or bug.<\/li>\n<li>Commit early and often, and push to a remote often to encourage collaboration as well as to provide a backup of your code.<\/li>\n<li>You can use settings within services like GitHub and Bitbucket to only allow certain users to push to the master and develop branches, and restrict other Developers to only commit and push to feature branches. Changes can then be committed and pushed, then reviewed as part of a peer code review, and merged back into the develop branch.<\/li>\n<\/ul>\n\n<h2 id=\"feedback\">Feedback<\/h2>\n\n<p>If you've got any questions, please feel free to <a href=\"http:\/\/twitter.com\/opdavies\" title=\"My Twitter account\">tweet at me<\/a> or fill in the <a href=\"http:\/\/2014.drupalcamplondon.co.uk\/node\/add\/session-evaluation?nid=86&destination=node\/86\" title=\"The session evaluation form to submit feedback\">session evaluation form<\/a> that you can complete on the DrupalCamp London website.<\/p>\n\n<p>I've had some great feedback via Twitter:<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<p><a href=\"https:\/\/twitter.com\/opdavies\">@opdavies<\/a> <a href=\"https:\/\/twitter.com\/DrupalCampLDN\">@DrupalCampLDN<\/a> always had trouble with git. Your talk + Git flow has made it all very easy.<\/p>\n\n<p>— James Tombs (@jtombs) <a href=\"https:\/\/twitter.com\/jtombs\/statuses\/440108072078696449\">March 2, 2014<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<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>\n\n<p>— Greg Franklin (@gfranklin) <a href=\"https:\/\/twitter.com\/gfranklin\/statuses\/440104311276969984\">March 2, 2014<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n\n<p><div class=\"my-4 flex justify-center \">\n <blockquote\n class=\"twitter-tweet\"\n lang=\"en\"\n data-cards=\"hidden\" >\n <\/p>\n\n<p>Great talk on git flow <a href=\"https:\/\/twitter.com\/opdavies\">@opdavies<\/a> <a href=\"https:\/\/twitter.com\/search?q=%23dclondon&src=hash\">#dclondon<\/a><\/p>\n\n<p>— Curve Agency (@CurveAgency) <a href=\"https:\/\/twitter.com\/CurveAgency\/statuses\/440095250775035904\">March 2, 2014<\/a>\n <\/blockquote>\n<\/div>\n<\/p>\n",
|
||
"tags": ["git"
|
||
,"git-flow"
|
||
,"drupalcamp-london"
|
||
,"talks"
|
||
]
|
||
}, {
|
||
"title": "DrupalCamp London 2014",
|
||
"path": "/articles/drupalcamp-london-2014",
|
||
"is_draft": "false",
|
||
"created": "1391904000",
|
||
"excerpt": "It's all booked, I'm going to be attending DrupalCamp London.",
|
||
"body": "<p>It's all booked, I'm going to be attending <a href=\"http:\/\/2014.drupalcamplondon.co.uk\">DrupalCamp London<\/a> this year, my first DrupalCamp!<\/p>\n\n<p>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.<\/p>\n\n<p>I'm looking forward to meeting some new people as well as catching up with some people that I met at <a href=\"http:\/\/prague2013.drupal.org\">DrupalCon Prague<\/a>.<\/p>\n\n<p>If you're also coming, see you there!<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupalcamp-london"
|
||
,"git"
|
||
,"git-flow"
|
||
]
|
||
}, {
|
||
"title": "Some Useful Git Aliases",
|
||
"path": "/articles/some-useful-git-aliases",
|
||
"is_draft": "false",
|
||
"created": "1389744000",
|
||
"excerpt": "Here are some bash aliases that I use and find helpful for quickly writing Git and Git Flow commands.",
|
||
"body": "<p>Here are some bash aliases that I use and find helpful for quickly writing Git and Git Flow commands.<\/p>\n\n<p>These should be placed within your <code>~\/.bashrc<\/code> or <code>~\/.bash_profile<\/code> file:<\/p>\n\n<pre><code class=\"language-bash\">alias gi=\"git init\"\nalias gcl=\"git clone\"\nalias gco=\"git checkout\"\nalias gs=\"git status\"\nalias ga=\"git add\"\nalias gaa=\"git add --all\"\nalias gc=\"git commit\"\nalias gcm=\"git commit -m\"\nalias gca=\"git commit -am\"\nalias gm=\"git merge\"\nalias gr=\"git rebase\"\nalias gps=\"git push\"\nalias gpl=\"git pull\"\nalias gd=\"git diff\"\nalias gl=\"git log\"\nalias gfi=\"git flow init\"\nalias gff=\"git flow feature\"\nalias gfr=\"git flow release\"\nalias gfh=\"git flow hotfix\"\n<\/code><\/pre>\n",
|
||
"tags": ["git"
|
||
]
|
||
}, {
|
||
"title": "Download Different Versions of Drupal with Drush",
|
||
"path": "/articles/download-different-versions-drupal-drush",
|
||
"is_draft": "false",
|
||
"created": "1388448000",
|
||
"excerpt": "How to download different versions of Drupal core using Drush.",
|
||
"body": "<p>If you use <a href=\"https:\/\/raw.github.com\/drush-ops\/drush\/master\/README.md\" title=\"About Drush\">Drush<\/a>, it's likely that you've used the <code>drush pm-download<\/code> (or <code>drush dl<\/code> 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.<\/p>\n\n<p>But what if you don't want Drupal 7?<\/p>\n\n<p>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.<\/p>\n\n<p>By declarding the core version of Drupal, such as \"drupal-6\", Drush will download that instead.<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl drupal-6\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl drupal-6 --select\n$ drush dl drupal-6 --select --all\n<\/code><\/pre>\n\n<p>If you want the most recent development version, just type:<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl drupal-6.x\n<\/code><\/pre>\n\n<p>The same can be done for other core versions of Drupal, from Drupal 5 upwards.<\/p>\n\n<pre><code class=\"language-bash\"># This will download Drupal 5\n$ drush dl drupal-5\n# This will download Drupal 8\n$ drush dl drupal-8\n<\/code><\/pre>\n\n<p>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 <a href=\"http:\/\/drush.ws\/#pm-download,\" title=\"The entry for pm-download on drush.ws\">drush.ws<\/a>.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drush"
|
||
]
|
||
}, {
|
||
"title": "Quickly Apply Patches Using Git and curl or wget",
|
||
"path": "/articles/quickly-apply-patches-using-git-and-curl-or-wget",
|
||
"is_draft": "false",
|
||
"created": "1387843200",
|
||
"excerpt": "How to quickly download a patch file and apply it to a Git repository in one line",
|
||
"body": "<p>Testing a patch file is usually a two-step process. First you download the patch file from the source, and then you run a separate command to apply it.<\/p>\n\n<p>You can save time and typing by running the two commands on one line:<\/p>\n\n<pre><code class=\"language-bash\">$ curl http:\/\/drupal.org\/files\/[patch-name].patch | git apply -v\n<\/code><\/pre>\n\n<p>Or, if you don't have curl installed, you can use wget:<\/p>\n\n<pre><code class=\"language-bash\">$ wget -q -O - http:\/\/drupal.org\/files\/[patch-name].patch | git apply -v\n<\/code><\/pre>\n\n<p>These commands need to be run within the root of your Git repository (i.e. where the .git directory is).<\/p>\n\n<p>These snippets were taken from <a href=\"https:\/\/drupal.org\/node\/1399218\">Applying Patches with Git<\/a> on Drupal.org.<\/p>\n",
|
||
"tags": ["git"
|
||
,"drupal-planet"
|
||
]
|
||
}, {
|
||
"title": "Useful Vagrant Commands",
|
||
"path": "/articles/useful-vagrant-commands",
|
||
"is_draft": "false",
|
||
"created": "1385510400",
|
||
"excerpt": "Here are the basic commands that you need to adminster a virtual machine using <a href=\"http:\/\/vagrantup.com\" title=\"The Vagrant Home page\">Vagrant<\/a>.",
|
||
"body": "<p><a href=\"http:\/\/www.vagrantup.com\" title=\"About Vagrant\">Vagrant<\/a> is a tool for managing virtual machines within <a href=\"https:\/\/www.virtualbox.org\">VirtualBox<\/a> from the command line. Here are some useful commands to know when using Vagrant.<\/p>\n\n<table>\n<thead>\n<tr>\n <th align=\"left\">Command<\/th>\n <th align=\"left\">Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n <td align=\"left\">vagrant init {box}<\/td>\n <td align=\"left\">Initialise a new VM in the current working directory. Specify a box name, or \"base\" will be used by default.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant status<\/td>\n <td align=\"left\">Shows the status of the Vagrant box(es) within the current working directory tree.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant up (--provision)<\/td>\n <td align=\"left\">Boots the Vagrant box. Including \"\u2013provision\" also runs the \"vagrant provision\" command.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant reload (--provision)<\/td>\n <td align=\"left\">Reloads the Vagrant box. Including \"--provision\" also runs the \"vagrant provision\" command.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant provision<\/td>\n <td align=\"left\">Provision the Vagrant box using Puppet.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant suspend<\/td>\n <td align=\"left\">Suspend the Vagrant box. Use \"vagrant up\" to start the box again.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant halt (-f)<\/td>\n <td align=\"left\">Halt the Vagrant box. Use -f to forcefully shut down the box without prompting for confirmation.<\/td>\n<\/tr>\n<tr>\n <td align=\"left\">vagrant destroy (-f)<\/td>\n <td align=\"left\">Destroys a Vagrant box. Use -f to forcefully shut down the box without prompting for confirmation.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n\n<p>The full Vagrant documentation can be found at <a href=\"http:\/\/docs.vagrantup.com\/v2\/\">http:\/\/docs.vagrantup.com\/v2\/<\/a>.<\/p>\n",
|
||
"tags": ["vagrant"
|
||
]
|
||
}, {
|
||
"title": "Don't Bootstrap Drupal, Use Drush",
|
||
"path": "/articles/dont-bootstrap-drupal-use-drush",
|
||
"is_draft": "false",
|
||
"created": "1384819200",
|
||
"excerpt": "Avoid bootstrapping Drupal manually in your scratch files - Drush has you covered!",
|
||
"body": "<p>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 <code>node_load()<\/code> and <code>db_query()<\/code>.<\/p>\n\n<p>To bootstrap Drupal, you would need to add some additional lines of code to the stop of your script. Here is an alternative way.<\/p>\n\n<pre><code class=\"language-php\"><?php\n\n\/\/ Bootstrap Drupal.\n$drupal_path = $_SERVER['DOCUMENT_ROOT'];\ndefine('DRUPAL_ROOT', $drupal_path);\nrequire_once DRUPAL_ROOT . '\/includes\/bootstrap.inc';\ndrupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);\n\n\/\/ Do stuff.\n$node = node_load(1);\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ drush scr foo.php\n<\/code><\/pre>\n\n<p>It also means that I no longer need to manually bootstrap Drupal, so my script is much cleaner.<\/p>\n\n<pre><code class=\"language-php\">\/\/ Just do stuff.\n$node = node_load(1);\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ drush scr ..\/scripts\/foo\n<\/code><\/pre>\n\n<p>Or, if you're using <a href=\"http:\/\/deeson-online.co.uk\/labs\/drupal-drush-aliases-and-how-use-them\" title=\"Drupal, Drush aliases, and how to use them\">Drush aliases<\/a>:<\/p>\n\n<pre><code class=\"language-bash\">$ drush @mysite.local scr foo\n<\/code><\/pre>\n\n<p>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 <a href=\"http:\/\/git-scm.com\/book\/en\/Git-Tools-Submodules\" title=\"Git Submodules\">Git submodule<\/a>.<\/p>\n",
|
||
"tags": ["drush"
|
||
,"drupal-planet"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "Create a Zen Sub-theme Using Drush",
|
||
"path": "/articles/create-zen-sub-theme-using-drush",
|
||
"is_draft": "false",
|
||
"created": "1378425600",
|
||
"excerpt": "How to quickly create a Zen sub-theme using Drush.",
|
||
"body": "<p>How to use <a href=\"https:\/\/drupal.org\/project\/drush\">Drush<\/a> to quickly build a new sub-theme of <a href=\"https:\/\/drupal.org\/project\/zen\">Zen<\/a>.<\/p>\n\n<p>First, download the <a href=\"https:\/\/drupal.org\/project\/zen\" title=\"The Zen theme\">Zen<\/a> theme if you haven't already done so.<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl zen\n<\/code><\/pre>\n\n<p>This will now enable you to use the \"drush zen\" command.<\/p>\n\n<pre><code class=\"language-bash\">$ drush zen \"Oliver Davies\" oliverdavies --description=\"A Zen sub-theme for oliverdavies.co.uk\" --without-rtl\n<\/code><\/pre>\n\n<p>The parameters that I'm passing it are:<\/p>\n\n<ol>\n<li>The human-readable name of the theme.<\/li>\n<li>The machine-readable name of the theme.<\/li>\n<li>The description of the theme (optional).<\/li>\n<li>A flag telling Drush not to include any right-to-left elements within my sub-theme as these aren't needed (optional).<\/li>\n<\/ol>\n\n<p>This will create a new theme in sites\/all\/themes\/oliverdavies.<\/p>\n\n<p>For further help, type <code>$ drush help zen<\/code> to see the Drush help page for the zen command.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drush"
|
||
,"zen"
|
||
,"theming"
|
||
]
|
||
}, {
|
||
"title": "Going to DrupalCon",
|
||
"path": "/articles/going-drupalcon",
|
||
"is_draft": "false",
|
||
"created": "1374796800",
|
||
"excerpt": "Precedent are sending myself and two of our other Drupal Developers to Drupalcon Prague.",
|
||
"body": "<p><a href=\"http:\/\/www.precedent.co.uk\">Precedent<\/a> are sending myself and two of our other Drupal Developers to <a href=\"http:\/\/prague2013.drupal.org\">Drupalcon Prague<\/a>.<\/p>\n\n<p>Having wanted to attend the last few Drupalcons (London, especially) but not being able to, I'm definitely looking forward to this one.<\/p>\n\n<p>See you there!<\/p>\n",
|
||
"tags": ["drupalcon"
|
||
,"precedent"
|
||
]
|
||
}, {
|
||
"title": "Creating Local and Staging sites with Drupal's Domain Module Enabled",
|
||
"path": "/articles/creating-local-and-staging-sites-drupals-domain-module-enabled",
|
||
"is_draft": "false",
|
||
"created": "1374019200",
|
||
"excerpt": "How to use aliases within Domain module for pre-production sites.",
|
||
"body": "<p>The <a href=\"https:\/\/drupal.org\/project\/domain\" title=\"The Domain Access project on Drupal.org\">Domain Access project<\/a> 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.<\/p>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-bash\">live_domain\nlocal_domain\nstaging_domain\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\">$databases['default']['default'] = array(\n 'driver' => 'mysql',\n 'database' => 'foobar',\n 'username' => 'foo',\n 'password' => 'bar',\n 'host' => 'localhost',\n 'prefix' => array(\n 'default' => '',\n 'domain' => 'local_', \/\/ This will use the local_domain table.\n \/\/ Add any other prefixed tables here.\n ),\n);\n<\/code><\/pre>\n\n<p>Within each environment-specific domain table, update the subdomain column to contain the appropriate domain names.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"databases"
|
||
,"domain"
|
||
,"table-prefixing"
|
||
]
|
||
}, {
|
||
"title": "Some useful links for using SimpleTest in Drupal",
|
||
"path": "/articles/some-useful-links-using-simpletest-drupal",
|
||
"is_draft": "false",
|
||
"created": "1371081600",
|
||
"excerpt": "Here are some useful links that I've found when researching about unit testing in Drupal using SimpleTest.",
|
||
"body": "<ul>\n<li><a href=\"http:\/\/www.lullabot.com\/blog\/articles\/introduction-unit-testing-drupal\" title=\"An Introduction to Unit Testing in Drupal\">An Introduction to Unit Testing in Drupal<\/a><\/li>\n<li><a href=\"http:\/\/www.lullabot.com\/blog\/articles\/drupal-module-developers-guide-simpletest\" title=\"Module Developer's Guide to SimpleTest\">Module Developer's Guide to SimpleTest<\/a><\/li>\n<li><a href=\"https:\/\/drupal.org\/simpletest-tutorial\" title=\"SimpleTest Tutorial (Drupal 6)\">SimpleTest Tutorial (Drupal 6)<\/a><\/li>\n<li><a href=\"https:\/\/drupal.org\/simpletest-tutorial-drupal7\" title=\"SimpleTest Tutorial (Drupal 7)\">SimpleTest Tutorial (Drupal 7)<\/a><\/li>\n<li><a href=\"https:\/\/drupal.org\/node\/278126\" title=\"SimpleTest Reference\">SimpleTest Reference<\/a><\/li>\n<li><a href=\"https:\/\/drupal.org\/node\/1128366\" title=\"Testing with SimpleTest\">Testing with SimpleTest<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["simpletest"
|
||
,"tdd"
|
||
,"test-driven-development"
|
||
,"drupal-planet"
|
||
,"drupal"
|
||
,"testing"
|
||
]
|
||
}, {
|
||
"title": "Display Git Branch or Tag Names in your Bash Prompt",
|
||
"path": "/articles/display-git-branch-or-tag-names-your-bash-prompt",
|
||
"is_draft": "false",
|
||
"created": "1367020800",
|
||
"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.",
|
||
"body": "<p>Whilst watching <a href=\"http:\/\/drupalize.me\" title=\"Drupalize.me\">Drupalize.me<\/a>'s recent <a href=\"http:\/\/drupalize.me\/series\/introduction-git-series\" title=\"Introduction to Git on Drupalize.me\">Introduction to Git series<\/a>, I thought it was useful the way that the current Git branch or tag name was displayed in the bash prompt.<\/p>\n\n<p>Here's how to do it.<\/p>\n\n<p>For example (with some slight modifications):<\/p>\n\n<pre><code class=\"language-bash\">oliver@oliver-mbp:~\/Development\/drupal(master) $\noliver@oliver-mbp:~\/Development\/a11y_checklist(7.x-1.0) $\n<\/code><\/pre>\n\n<p>Here's how to do it.<\/p>\n\n<p>To begin with, create a new file to contain the functions,<\/p>\n\n<pre><code class=\"language-bash\">vim ~\/.bash\/git-prompt\n<\/code><\/pre>\n\n<p>Paste the following code into the file, and save it.<\/p>\n\n<pre><code class=\"language-bash\">parse_git_branch () {\n git branch 2> \/dev\/null | sed -e '\/^[^*]\/d' -e 's\/* \\(.*\\)\/ (\\1)\/'\n}\n\nparse_git_tag () {\n git describe --tags 2> \/dev\/null\n}\n\nparse_git_branch_or_tag() {\n local OUT=\"$(parse_git_branch)\"\n if [ \"$OUT\" == \" ((no branch))\" ]; then\n OUT=\"($(parse_git_tag))\";\n fi\n echo $OUT\n}\n<\/code><\/pre>\n\n<p>Edit your <code>.bashrc<\/code> or <code>.bash_profile<\/code> file to override the PS1 value.<\/p>\n\n<pre><code class=\"language-bash\">vim ~\/.bashrc\n<\/code><\/pre>\n\n<p>Add the following code at the bottom of the file, and save it.<\/p>\n\n<pre><code class=\"language-bash\">source ~\/.bash\/git-prompt\nPS1=\"\\u@\\h:\\w\\$(parse_git_branch_or_tag) $ \"\n<\/code><\/pre>\n\n<p>Restart your Terminal or type <code>source ~\/.bashrc<\/code> to see your changes.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"git"
|
||
,"terminal"
|
||
]
|
||
}, {
|
||
"title": "Leaving Nomensa, Joining Precedent",
|
||
"path": "/articles/leaving-nomensa-joining-precedent",
|
||
"is_draft": "false",
|
||
"created": "1366416000",
|
||
"excerpt": "Yesterday was my last day working at Nomensa. Next week, I'll be starting as a Senior Developer at Precedent.",
|
||
"body": "<p>Yesterday was my last day working at <a href=\"http:\/\/www.nomensa.com\" title=\"Nomensa\">Nomensa<\/a>. Next week, I'll be starting as a Senior Developer at <a href=\"http:\/\/www.precedent.co.uk\" title=\"Precedent\">Precedent<\/a>.<\/p>\n\n<p>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 <a href=\"http:\/\/www.unionlearn.org\" title=\"unionlearn\">unionlearn<\/a> and <a href=\"http:\/\/www.digitaltheatreplus.com\" title=\"Digital Theatre Plus\">Digital Theatre Plus<\/a>. 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 <a href=\"http:\/\/drupal.org\/project\/nomensa_amp\" title=\"The Nomensa Accessible Media Player Drupal module\">Nomensa Accessible Media Player<\/a> module and the <a href=\"http:\/\/drupal.org\/project\/a11y_checklist\" title=\"The accessibility checklist for Drupal\">Accessibility Checklist<\/a>. I'll definitely be continuing my interest in accessibility, championing best practices, and incorporating it into my future work wherever possible.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["nomensa"
|
||
,"precedent"
|
||
,"personal"
|
||
]
|
||
}, {
|
||
"title": "The Quickest way to Install Sublime Text 2 in Ubuntu",
|
||
"path": "/articles/quickest-way-install-sublime-text-2-ubuntu",
|
||
"is_draft": "false",
|
||
"created": "1362182400",
|
||
"excerpt": "After reading numerous blog posts about how to install Sublime Text 2 in Ubuntu, this is definitely the quickest way!",
|
||
"body": "<p>After reading numerous blog posts about how to install <a href=\"http:\/\/www.sublimetext.com\/2\" title=\"Sublime Text 2\">Sublime Text 2<\/a> in <a href=\"http:\/\/www.ubuntu.com\/2\" title=\"Ubuntu\">Ubuntu<\/a>, this is definitely the quickest way!<\/p>\n\n<p>Just paste the following lines into your Terminal:<\/p>\n\n<pre><code class=\"language-bash\">$ sudo add-apt-repository ppa:webupd8team\/sublime-text-2\n$ sudo apt-get update\n$ sudo apt-get install sublime-text\n<\/code><\/pre>\n\n<p>After running this, Sublime Text 2 has been installed within the <em>\/usr\/lib\/sublime-text-2<\/em> directory and can be launched from the Dashboard, or by typing <code>subl<\/code>, <code>sublime-text<\/code> or <code>sublime-text-2<\/code> into a Terminal window.<\/p>\n",
|
||
"tags": ["linux"
|
||
,"sublime-text"
|
||
,"ubuntu"
|
||
]
|
||
}, {
|
||
"title": "Creating and using custom tokens in Drupal 7",
|
||
"path": "/articles/creating-and-using-custom-tokens-drupal-7",
|
||
"is_draft": "false",
|
||
"created": "1360972800",
|
||
"excerpt": "This post outlines the steps required to create your own custom tokens in Drupal.",
|
||
"body": "<p>This post outlines the steps required to create your own custom tokens in Drupal.<\/p>\n\n<p>When writing the recent releases of the <a href=\"http:\/\/drupal.org\/project\/copyright_block\">Copyright Block<\/a> 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.<\/p>\n\n<p>We will be using the fictional <em>foo<\/em> module to demonstrate this.<\/p>\n\n<h2 id=\"requirements\">Requirements<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/drupal.org\/project\/token\">Token module<\/a><\/li>\n<\/ul>\n\n<h2 id=\"recommended\">Recommended<\/h2>\n\n<ul>\n<li><a href=\"http:\/\/drupal.org\/project\/devel\">Devel module<\/a> - useful to run <code>dpm()<\/code> and <code>kpr()<\/code> functions<\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/copyright_block\">Copyright Block module<\/a> - 7.x-2.x and 6.x-1.x use tokens, handy as a reference<\/li>\n<\/ul>\n\n<h2 id=\"implementing-hook_token_info\">Implementing hook_token_info()<\/h2>\n\n<p>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 <code>dpm(token_get_info());<\/code>, assuming that you have the <a href=\"http:\/\/drupal.org\/project\/devel\">Devel module<\/a> installed.<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implements hook_token_info().\n *\/\nfunction foo_token_info() {\n $info = array();\n\n \/\/ Add any new tokens.\n $info['tokens']['foo']['bar'] = t('This is my new bar token within the foo type.');\n\n \/\/ Return them.\n return $info;\n}\n<\/code><\/pre>\n\n<p>In this case, the token called <em>bar<\/em> resides within the <em>foo<\/em> group.<\/p>\n\n<p>If I needed to add a new token within an existing token type, such as 'node', the syntax would be <code>$info['tokens']['node']['bar']<\/code>.<\/p>\n\n<h2 id=\"implementing-hook_tokens\">Implementing hook_tokens()<\/h2>\n\n<p>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 <code>hook_tokens()<\/code>. Here is the basic code needed for an implementation:<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implements hook_tokens().\n *\/\nfunction foo_tokens($type, $tokens, array $data = array(), array $options = array()) {\n $replacements = array();\n\n \/\/ Code goes here...\n\n \/\/ Return the replacements.\n return $replacements;\n}\n<\/code><\/pre>\n\n<p>The first thing to check for is the type of token using an <code>if()<\/code> 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 <code>foreach ($tokens as $name => $original)<\/code> to loop through each of the available tokens using a <code>switch()<\/code>. For each token, you can perform some logic to work out the replacement text and then add it into the replacements array using <code>$replacements[$original] = $new;<\/code>.<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implements hook_tokens().\n *\/\nfunction foo_tokens($type, $tokens, array $data = array(), array $options = array()) {\n $replacements = array();\n\n \/\/ The first thing that we're going to check for is the type of token - node,\n \/\/ user etc...\n if ($type == 'foo') {\n \/\/ Loop through each of the available tokens.\n foreach ($tokens as $name => $original) {\n \/\/ Find the desired token by name\n switch ($name) {\n case 'bar':\n $new = '';\n\n \/\/ Work out the value of $new...\n\n \/\/ Add the new value into the replacements array.\n $replacements[$original] = $new;\n break;\n }\n }\n }\n\n \/\/ Return the replacements.\n return $replacements;\n}\n<\/code><\/pre>\n\n<h2 id=\"example\">Example<\/h2>\n\n<p>An example from Copyright Block module:<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implements hook_tokens().\n *\/\nfunction copyright_block_tokens($type, $tokens, array $data = array(), array $options = array()) {\n $replacements = array();\n\n if ($type == 'copyright_statement') {\n foreach ($tokens as $name => $original) {\n switch ($name) {\n case 'dates':\n $start_year = variable_get('copyright_block_start_year', date('Y'));\n $current_year = date('Y');\n\n $replacements[$original] = $start_year < $current_year ? $start_year . '-' . $current_year : $start_year;\n break;\n }\n }\n }\n\n return $replacements;\n}\n<\/code><\/pre>\n\n<h2 id=\"using-token_replace\">Using token_replace()<\/h2>\n\n<p>With everything defined, all that we now need to do is pass some text through the <code>token_replace()<\/code> function to replace it with the values defined within <code>hook_token()<\/code>.<\/p>\n\n<pre><code class=\"language-php\">$a = t('Something');\n\/\/ This would use any token type - node, user etc.\n$b = token_replace($a);\n\/\/ This would only use foo tokens.\n$c = token_replace($a, array('foo'));\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"drupal-7"
|
||
,"tokens"
|
||
]
|
||
}, {
|
||
"title": "Checking if a user is logged into Drupal (the right way)",
|
||
"path": "/articles/checking-if-user-logged-drupal-right-way",
|
||
"is_draft": "false",
|
||
"created": "1357689600",
|
||
"excerpt": "How to check if a user is logged in by using Drupal core API functions.",
|
||
"body": "<p>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).<\/p>\n\n<pre><code class=\"language-php\">global $user;\nif ($user->uid) {\n \/\/ The user is logged in.\n}\n<\/code><\/pre>\n\n<p>or<\/p>\n\n<pre><code class=\"language-php\">global $user;\nif (!$user->uid) {\n \/\/ The user is not logged in.\n}\n<\/code><\/pre>\n\n<p>The better way to do this is to use the <a href=\"http:\/\/api.drupal.org\/api\/drupal\/modules!user!user.module\/function\/user_is_logged_in\/7\">user_is_logged_in()<\/a> function.<\/p>\n\n<pre><code class=\"language-php\">if (user_is_logged_in()) {\n \/\/ Do something.\n}\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>A great use case for this is within a <code>hook_menu()<\/code> implementation within a custom module.<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implements hook_menu().\n *\/\nfunction mymodule_menu() {\n $items['foo'] = array(\n 'title' => 'Foo',\n 'page callback' => 'mymodule_foo',\n 'access callback' => 'user_is_logged_in',\n );\n\n return $items;\n}\n<\/code><\/pre>\n\n<p>There is also a <a href=\"http:\/\/api.drupal.org\/api\/drupal\/modules!user!user.module\/function\/user_is_anonymous\/7\">user_is_anonymous()<\/a> function if you want the opposite result. Both of these functions are available in Drupal 6 and higher.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-6"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"php"
|
||
]
|
||
}, {
|
||
"title": "How to use SASS and Compass in Drupal 7 using Sassy",
|
||
"path": "/articles/use-sass-and-compass-drupal-7-using-sassy",
|
||
"is_draft": "false",
|
||
"created": "1354752000",
|
||
"excerpt": "Use PHPSass and the Sassy module to use Sass and Compass in your Drupal theme.",
|
||
"body": "<p>I've recently started using <a href=\"http:\/\/sass-lang.com\">SASS<\/a> rather than LESS to do my CSS preprocessing - namely due to its integration with <a href=\"http:\/\/compass-style.org\">Compass<\/a> and it's built-in CSS3 mixins. Here are three modules that provide the ability to use SASS within Drupal.<\/p>\n\n<ul>\n<li><a href=\"http:\/\/drupal.org\/project\/sassy\" title=\"Sassy module on drupal.org\">Sassy<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/prepro\" title=\"Prepro module on drupal.org\">Prepro<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/libraries\" title=\"Libraries API module on drupal.org\">Libraries API<\/a><\/li>\n<\/ul>\n\n<p>Alternatively, you could use a base theme like <a href=\"http:\/\/drupal.org\/project\/sasson\" title=\"Sasson theme on drupal.org\">Sasson<\/a> that includes a SASS compiler.<\/p>\n\n<h2 id=\"download-the-phpsass-library\">Download the PHPSass Library<\/h2>\n\n<p>The first thing to do is download the PHPSass library from <a href=\"https:\/\/github.com\/richthegeek\/phpsass\" title=\"PHPSass on GitHub\">GitHub<\/a>, as this is a requirement of the Sassy module and we can't enable it without the library. So, in a Terminal window:<\/p>\n\n<pre><code class=\"language-bash\">$ mkdir -p sites\/all\/libraries; \n$ cd sites\/all\/libraries; \n$ wget https:\/\/github.com\/richthegeek\/phpsass\/archive\/master.tar.gz; \n$ tar zxf master.tar.gz; \n$ rm master.tar.gz; \n$ mv phpsass-master\/ phpsass\n<\/code><\/pre>\n\n<p>Or, if you're using Drush Make files:<\/p>\n\n<pre><code class=\"language-ini\">libraries[phpsass][download][type] = \"get\"\nlibraries[phpsass][download][url] = \"https:\/\/github.com\/richthegeek\/phpsass\/archive\/master.tar.gz\"\n<\/code><\/pre>\n\n<p>The PHPSass library should now be located at <code>sites\/all\/libraries\/phpsass<\/code>.<\/p>\n\n<h2 id=\"download-and-enable-the-drupal-modules\">Download and enable the Drupal modules<\/h2>\n\n<p>This is easy if you use <a href=\"http:\/\/drupal.org\/project\/drush\">Drush<\/a>:<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl libraries prepro sassy\n$ drush en -y libraries prepro sassy sassy_compass\n<\/code><\/pre>\n\n<p>Otherwise, download the each module from it's respective project page and place it within your <code>sites\/all\/modules<\/code> or <code>sites\/all\/modules\/contrib<\/code> directory.<\/p>\n\n<h2 id=\"configuring-the-prepro-module\">Configuring the Prepro module<\/h2>\n\n<p>The Prepro module provides various settings that can be changed for each preprocessor. Go to <code>admin\/config\/media\/prepro<\/code> to configure the module as required.<\/p>\n\n<p>Personally, in development, I'd set caching to 'uncached' and the error reporting method to 'show on page'. In production, I'd change these to \"cached\" and \"watchdog\" respectively. I'd also set the output style to \"compressed\",<\/p>\n\n<h2 id=\"adding-sass-files-into-your-theme\">Adding SASS files into your theme<\/h2>\n\n<p>With this done, you can now add SASS and SCSS files by adding a line like <code>stylesheets[all][] = css\/base.scss<\/code> in your theme's .info file.<\/p>\n",
|
||
"tags": ["compass"
|
||
,"css"
|
||
,"drupal"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"less"
|
||
,"preprocessing"
|
||
,"sass"
|
||
]
|
||
}, {
|
||
"title": "Open Sublime Text 2 from the Mac OS X Command Line",
|
||
"path": "/articles/open-sublime-text-2-mac-os-x-command-line",
|
||
"is_draft": "false",
|
||
"created": "1353110400",
|
||
"excerpt": "How to open Sublime Text from the command line.",
|
||
"body": "<p>How to open Sublime Text from the command line.<\/p>\n\n<p>Paste the following code into the Mac OS X Terminal, assuming that you've installed Sublime Text 2 into the \/Applications folder.<\/p>\n\n<pre><code class=\"language-bash\">$ ln -s \"\/Applications\/Sublime Text 2.app\/Contents\/SharedSupport\/bin\/subl\" ~\/bin\/sublime\n<\/code><\/pre>\n\n<p>Now you can type <code>sublime <filename><\/code> open a file or directory in Sublime Text, or <code>sublime .<\/code> to open the current directory.<\/p>\n\n<p>You can also type <code>sublime --help<\/code> to see a list of the available commands.<\/p>\n",
|
||
"tags": ["sublime-text"
|
||
,"mac-os-x"
|
||
,"terminal"
|
||
]
|
||
}, {
|
||
"title": "Accessible Bristol site launched",
|
||
"path": "/articles/accessible-bristol-site",
|
||
"is_draft": "false",
|
||
"created": "1352937600",
|
||
"excerpt": "I'm happy to report that the Accessible Bristol was launched this week, on Drupal 7.",
|
||
"body": "<p>I'm happy to announce that the <a href=\"http:\/\/www.accessiblebristol.org.uk\">Accessible Bristol<\/a> website was launched this week, on Drupal 7. The site has been developed over the past few months, and uses the <a href=\"http:\/\/drupal.org\/project\/user_relationships\">User Relationships<\/a> and <a href=\"http:\/\/drupal.org\/project\/privatemsg\">Privatemsg<\/a> modules to provide a community-based platform where people with an interest in accessibility can register and network with each other.<\/p>\n\n<p>The site has been developed over the past few months, and uses the <a href=\"http:\/\/drupal.org\/project\/user_relationships\">User Relationships<\/a> and <a href=\"http:\/\/drupal.org\/project\/privatemsg\">Privatemsg<\/a> modules to provide a community-based platform where people with an interest in accessibility can register and network with each other.<\/p>\n\n<p>The group is hosting a launch event on the 28th November at the Council House, College Green, Bristol. Interested? More information is available at <a href=\"http:\/\/www.accessiblebristol.org.uk\/events\/accessible-bristol-launch\">http:\/\/www.accessiblebristol.org.uk\/events\/accessible-bristol-launch<\/a> or go to <a href=\"http:\/\/buytickets.at\/accessiblebristol\/6434\">http:\/\/buytickets.at\/accessiblebristol\/6434<\/a> to register.<\/p>\n",
|
||
"tags": ["accessibility"
|
||
,"accessible-bristol"
|
||
,"nomensa"
|
||
]
|
||
}, {
|
||
"title": "My Sublime Text 2 settings",
|
||
"path": "/articles/my-sublime-text-2-settings",
|
||
"is_draft": "false",
|
||
"created": "1351123200",
|
||
"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.",
|
||
"body": "<p><a href=\"http:\/\/www.sublimetext.com\/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.<\/p>\n\n<p>These preferences ensure that the code is compliant with <a href=\"http:\/\/drupal.org\/coding-standards\" title=\"Drupal coding standards on Drupal.org\">Drupal coding standards<\/a> - using two spaces instead of a tab, no trailing whitespace, blank line at the end of a file etc.<\/p>\n\n<h2 id=\"preferences\">Preferences<\/h2>\n\n<p>These can be changed by going to Preferences > Settings - User.<\/p>\n\n<pre><code class=\"language-json\">{\n \"color_scheme\": \"Packages\/Theme - Aqua\/Color Schemes\/Tomorrow Night Aqua.tmTheme\",\n \"default_line_ending\": \"unix\",\n \"ensure_newline_at_eof_on_save\": true,\n \"fallback_encoding\": \"UTF-8\",\n \"file_exclude_patterns\":\n [\n \"*.pyc\",\n \"*.pyo\",\n \"*.exe\",\n \"*.dll\",\n \"*.obj\",\n \"*.o\",\n \"*.a\",\n \"*.lib\",\n \"*.so\",\n \"*.dylib\",\n \"*.ncb\",\n \"*.sdf\",\n \"*.suo\",\n \"*.pdb\",\n \"*.idb\",\n \".DS_Store\",\n \"*.class\",\n \"*.psd\",\n \"*.db\",\n \"*.sublime*\"\n ],\n \"folder_exclude_patterns\":\n [\n \".svn\",\n \".git\",\n \".hg\",\n \"CVS\",\n \"FirePHPCore\"\n ],\n \"font_options\":\n [\n \"no_bold\",\n \"no_italic\"\n ],\n \"font_size\": 16.0,\n \"highlight_line\": true,\n \"ignored_packages\":\n [\n ],\n \"line_padding_bottom\": 1,\n \"rulers\":\n [\n 80\n ],\n \"save_on_focus_lost\": true,\n \"shift_tab_unindent\": true,\n \"tab_size\": 2,\n \"theme\": \"Soda Light.sublime-theme\",\n \"translate_tabs_to_spaces\": true,\n \"trim_automatic_white_space\": true,\n \"trim_trailing_white_space_on_save\": true,\n \"word_wrap\": false\n}\n<\/code><\/pre>\n\n<h2 id=\"key-bindings\">Key bindings<\/h2>\n\n<p>These can be changed by going to Preferences > Key Bindings - User.<\/p>\n\n<pre><code class=\"language-json\">[\n { \"keys\": [\"alt+s\"], \"command\": \"toggle_side_bar\" },\n { \"keys\": [\"alt+r\"], \"command\": \"reindent\" }\n]\n<\/code><\/pre>\n\n<h2 id=\"packages\">Packages<\/h2>\n\n<p>These are the packages that I currently have installed.<\/p>\n\n<ul>\n<li><a href=\"https:\/\/github.com\/spadgos\/sublime-jsdocs\" title=\"DocBlockr on GitHub\">DocBlockr<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/BrianGilbert\/Sublime-Text-2-Goto-Drupal-API\">Drupal API<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/danro\/LESS-sublime\">LESS<\/a><\/li>\n<li><a href=\"http:\/\/wbond.net\/sublime_packages\/package_control\">Package Control<\/a><\/li>\n<li><a href=\"http:\/\/github.com\/Kronuz\/SublimeCodeIntel\">Sublime CodeIntel<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/buymeasoda\/soda-theme\">Theme - Soda<\/a><\/li>\n<\/ul>\n",
|
||
"tags": ["sublime-text"
|
||
]
|
||
}, {
|
||
"title": "Reflections on speaking at UnifiedDiff",
|
||
"path": "/articles/reflections-speaking-unifieddiff",
|
||
"is_draft": "false",
|
||
"created": "1346889600",
|
||
"excerpt": "Yesterday evening I went along and spoke at the UnifiedDiff meetup in Cardiff.",
|
||
"body": "<p>Yesterday evening I went along and spoke at the <a href=\"http:\/\/www.unifieddiff.co.uk\">UnifiedDiff meetup<\/a> in Cardiff, having offered previously to do a presentation providing an introduction to Drupal.<\/p>\n\n<p>I'm an experienced Drupal Developer, but not an experienced public speaker (although I have done several user training sessions and Drupal demonstrations for clients previously), and I think that some of the nerves that I had beforehand were apparent during the presentation, and being the first speaker for the evening probably didn't help, although I did get a <a href=\"https:\/\/twitter.com\/craigmarvelley\/status\/243418608720543745\">nice tweet<\/a> mid-way through.<\/p>\n\n<p>Initially, after aiming for a 20-minute presentation plus Q&A, I think I wrapped up the presentation in around 14 minutes, although I did about 6 minutes of answering questions afterwards including the apparently mandatory \"Why use Drupal compared to WordPress or Joomla?\" question, some Drupal 8 and Symfony questions, as well as an interesting question about the White House development project after I'd listed it within a list of example sites. Next time, I think that some more detailed presenter notes are needed. Typically, as soon as it sat back in my seat, the majority of things that I'd managed to remember beforehand all came flooding back to me and I thought \"I should have said that whilst I was up speaking\".<\/p>\n\n<p>Overall, considering my inexperience at speaking to this type of audience, I was fairly happy with my presentation, although I'm sure that I'll change my mind once I've watched the video of it on the UnifiedDiff website. Regardless, I think that it was a great experience and I enjoyed doing it, and I'd like to thank the organisers of UnifiedDiff for having me speak at their meetup. It was great to have a more relaxed conversation with some people after the other speakers had been up, and having introduced Drupal I would be more than happy to come back and do a more in-depth presentation if there is an interest for me to do so.<\/p>\n",
|
||
"tags": ["talks"
|
||
]
|
||
}, {
|
||
"title": "Display a Custom Menu in a Drupal 7 Theme Template File",
|
||
"path": "/articles/display-custom-menu-drupal-7-theme-template-file",
|
||
"is_draft": "false",
|
||
"created": "1345248000",
|
||
"excerpt": "The code needed to display a menu in a Drupal 7 template file.",
|
||
"body": "<p>For reference, this is the code needed to display a menu in a Drupal 7 template file, including the navigation ARIA role.<\/p>\n\n<pre><code class=\"language-php\">$menu_name = 'menu-footer-menu';\n$menu_id = 'footer-menu';\nprint theme('links', array(\n 'links' => menu_navigation_links($menu_name),\n 'attributes' => array(\n 'id' => $menu_id,\n 'role' => 'navigation',\n 'class'=> array('links', 'inline')\n )\n));\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"php"
|
||
,"aria"
|
||
]
|
||
}, {
|
||
"title": "Writing an Article for Linux Journal",
|
||
"path": "/articles/writing-article-linux-journal",
|
||
"is_draft": "false",
|
||
"created": "1343347200",
|
||
"excerpt": "I'm absolutely delighted to announce that I'm going to be writing an article for Linux Journal magazine's upcoming Drupal special.",
|
||
"body": "<p>I'm absolutely delighted to announce that I'm going to be writing an article for <a href=\"http:\/\/www.linuxjournal.com\">Linux Journal<\/a> magazine's upcoming Drupal special.<\/p>\n\n<p>The article is going to be entitled \"Speeding Up Your Drupal Development Using Installation Profiles and Distributions\" and will be mentioning existing distributions available on Drupal.org, but mainly focussing on the steps needed to create your own custom distribution. Needless to say, I'm quite excited about it!<\/p>\n\n<p>The article is expected to be published in October.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"distributions"
|
||
,"installation-profiles"
|
||
,"writing"
|
||
,"linux-journal"
|
||
]
|
||
}, {
|
||
"title": "Install and Configure the Nomensa Accessible Media Player in Drupal",
|
||
"path": "/articles/install-nomensa-media-player-drupal",
|
||
"is_draft": "false",
|
||
"created": "1342224000",
|
||
"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.",
|
||
"body": "<p>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.<\/p>\n\n<p><em>The official documentation for this module is now located at <a href=\"https:\/\/www.drupal.org\/node\/2383447\">https:\/\/www.drupal.org\/node\/2383447<\/a>. This post was accurate at the time of writing, whereas the documentation page will be kept up to date with any future changes.<\/em><\/p>\n\n<h2 id=\"initial-configuration\">Initial configuration<\/h2>\n\n<h3 id=\"download-the-library\">Download the Library<\/h3>\n\n<p>The library can be downloaded directly from GitHub, and should be placed within you <em>sites\/all\/libraries\/nomensa_amp<\/em> directory.<\/p>\n\n<pre><code class=\"language-bash\">drush dl libraries nomensa_amp\ngit clone https:\/\/github.com\/nomensa\/Accessible-Media-Player sites\/all\/libraries\/nomensa_amp\ncd sites\/all\/libraries\/nomensa_amp\nrm -rf Accessible-media-player_2.0_documentation.pdf example\/ README.md\ndrush en -y nomensa_amp\n<\/code><\/pre>\n\n<h3 id=\"configure-the-module\">Configure the Module<\/h3>\n\n<p>Configure the module at <em>admin\/config\/media\/nomensa-amp<\/em> and enable the players that you want to use.<\/p>\n\n<h2 id=\"adding-videos\">Adding videos<\/h2>\n\n<p>Within your content add links to your videos. For example:<\/p>\n\n<h3 id=\"youtube\">YouTube<\/h3>\n\n<pre><code class=\"language-html\"><a href=\"http:\/\/www.youtube.com\/watch?v=Zi31YMGmQC4\">Checking colour contrast<\/a>\n<\/code><\/pre>\n\n<h3 id=\"vimeo\">Vimeo<\/h3>\n\n<pre><code class=\"language-html\"><a href=\"http:\/\/vimeo.com\/33729937\">Screen readers are strange, when you're a stranger by Leonie Watson<\/a>\n<\/code><\/pre>\n\n<h2 id=\"adding-captions\">Adding captions<\/h2>\n\n<p>The best way that I can suggest to do this is to use a File field to upload your captions file:<\/p>\n\n<ol>\n<li>Add a File field to your content type;<\/li>\n<li>On your page upload the captions file.<\/li>\n<li>Right-click the uploaded file, copy the link location, and use this for the path to your captions file.<\/li>\n<\/ol>\n\n<p>For example:<\/p>\n\n<pre><code class=\"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>\n<\/code><\/pre>\n\n<h2 id=\"screencast\">Screencast<\/h2>\n\n<div class=\"embed-container\">\n <iframe\n src=\"https:\/\/player.vimeo.com\/video\/45731954\"\n width=\"500\"\n height=\"313\"\n frameborder=\"0\"\n webkitallowfullscreen\n mozallowfullscreen\n allowfullscreen>\n <\/iframe>\n<\/div>\n",
|
||
"tags": ["accessibility"
|
||
,"drupal"
|
||
,"drupal-planet"
|
||
,"nomensa"
|
||
]
|
||
}, {
|
||
"title": "My new Drupal modules",
|
||
"path": "/articles/my-new-drupal-modules",
|
||
"is_draft": "false",
|
||
"created": "1342051200",
|
||
"excerpt": "After a busy few days, I've released two new contrib Drupal modules.",
|
||
"body": "<p>After a busy few days, I've released two new contrib Drupal modules.<\/p>\n\n<ul>\n<li><a href=\"http:\/\/drupal.org\/project\/block_aria_landmark_roles\">Block Aria Landmark Roles<\/a> - Inspired by <a href=\"http:\/\/drupal.org\/project\/block_class\">Block Class<\/a>, this module adds additional elements to the block configuration forms that allow users to assign a ARIA landmark role to a block.<\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/nomensa_amp\">Nomensa Accessible Media Player<\/a> - Provides integration with Nomensa's <a href=\"https:\/\/github.com\/nomensa\/Accessible-Media-Player\">Accessible Media Player<\/a>.<\/li>\n<\/ul>\n\n<p>Documentation for both to follow shortly on Drupal.org.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-modules"
|
||
,"drupal-6"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"accessibility"
|
||
]
|
||
}, {
|
||
"title": "Dividing Drupal's process and preprocess functions into separate files",
|
||
"path": "/articles/dividing-drupals-process-and-preprocess-functions-separate-files",
|
||
"is_draft": "false",
|
||
"created": "1337817600",
|
||
"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.",
|
||
"body": "<p>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 <a href=\"http:\/\/drupal.org\/project\/omega\" title=\"The Omega theme on Drupal.org\">Omega theme<\/a>, 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.<\/p>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-php\"><?php\n\n\/**\n * Implements hook_preprocess().\n * \n * Initialises the mytheme_invoke() function for the preprocess hook.\n *\/\nfunction mytheme_preprocess(&$variables, $hook) {\n mytheme_invoke('preprocess', $hook, $variables);\n}\n\n\/**\n * Implements hook_process().\n * \n * Initialises the mytheme_invoke() function for the process hook.\n *\/\nfunction mytheme_process(&$variables, $hook) {\n mytheme_invoke('process', $hook, $variables);\n}\n<\/code><\/pre>\n\n<p>Now, to write the <code>mytheme_invoke()<\/code> function:<\/p>\n\n<pre><code class=\"language-php\"><?php\n\n\/**\n * Invokes custom process and preprocess functions.\n *\n * @param string $type\n * The type of function we are trying to include (i.e. process or preprocess).\n *\n * @param array $variables\n * The variables array.\n *\n * @param string $hook\n * The name of the hook.\n * \n * @see mytheme_preprocess() \n * @see mytheme_process()\n *\/\nfunction mytheme_invoke($type, $hook, &$variables) {\n global $theme_key;\n\n \/\/ The name of the function to look for (e.g. mytheme_process_node).\n $function = $theme_key . '_' . $type . '_' . $hook;\n\n \/\/ If the function doesn't exist within template.php, look for the \n \/\/ appropriate include file.\n if (!function_exists($function)) {\n \/\/ The file to search for (e.g. process\/node.inc).\n $file = drupal_get_path('theme', $theme_key) . '\/' . $type . '\/' . $type . '-' . str_replace('_', '-', $hook) . '.inc';\n\n \/\/ If the file exists, include it.\n if (is_file($file)) {\n include($file);\n }\n }\n\n \/\/ Try to call the function again.\n if (function_exists($function)) {\n $function($variables);\n }\n}\n<\/code><\/pre>\n\n<p>As <code>mytheme_invoke()<\/code> 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.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"code"
|
||
,"theming"
|
||
,"preprocessing"
|
||
]
|
||
}, {
|
||
"title": "Writing a .info file for a Drupal 7 theme",
|
||
"path": "/articles/writing-info-file-drupal-7-theme",
|
||
"is_draft": "false",
|
||
"created": "1337731200",
|
||
"excerpt": "An example .info file for a Drupal 7 theme.",
|
||
"body": "<pre><code class=\"language-ini\">name = My Theme\ndescription = A description of my theme\ncore = 7.x\n\n# Add a base theme, if you want to use one.\nbase = mybasetheme\n\n# Define regions, otherwise the default regions will be used.\nregions[header] = Header\nregions[navigation] = Navigation\nregions[content] = Content\nregions[sidebar] = Sidebar\nregions[footer] = Footer\n\n# Define which features are available. If none are specified, all the default \n# features will be available.\nfeatures[] = logo\nfeatures[] = name\nfeatures[] = favicon\n\n# Add stylesheets\nstylesheets[all][] = css\/reset.css\nstylesheets[all][] = css\/mytheme.css\nstylesheets[print][] = css\/print.css\n\n# Add javascript files\nstyles[] = js\/mytheme.js\n<\/code><\/pre>\n",
|
||
"tags": ["theming"
|
||
,"drupal-theming"
|
||
,"drupal"
|
||
,"code"
|
||
]
|
||
}, {
|
||
"title": "Prevent Apache from displaying text files within a web browser",
|
||
"path": "/articles/prevent-apache-displaying-text-files-within-web-browser",
|
||
"is_draft": "false",
|
||
"created": "1337731200",
|
||
"excerpt": "How to prevent Apache from displaying the contents of files like CHANGELOG.txt.",
|
||
"body": "<p>When you download <a href=\"http:\/\/drupal.org\/project\/drupal\">Drupal<\/a>, 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.<\/p>\n\n<p>Rather than delete these files or change the file permissions manually for each file, I can add the following lines into my VirtualHost configuration.<\/p>\n\n<pre><code class=\"language-apacheconf\"><Files ~ \"\\.txt$\">\n Order deny,allow\n Deny from all\n<\/Files>\n<\/code><\/pre>\n\n<p>This prevents any files with a .txt extension from being accessed and rendered in a web browser.<\/p>\n",
|
||
"tags": ["apache"
|
||
,"code"
|
||
,"drupal"
|
||
]
|
||
}, {
|
||
"title": "How to add a date popup calendar onto a custom form",
|
||
"path": "/articles/add-date-popup-calendar-custom-form",
|
||
"is_draft": "false",
|
||
"created": "1337731200",
|
||
"excerpt": "How to use a date popup calendar within your custom module.",
|
||
"body": "<p>How to use a date popup calendar within your custom module.<\/p>\n\n<p>First, I need to download the <a href=\"http:\/\/drupal.org\/project\/date\" title=\"Date module on Drupal.org\">Date<\/a> module, and make my module dependent on date_popup by adding the following line into my module's .info file.<\/p>\n\n<pre><code class=\"language-ini\">dependencies[] = date_popup\n<\/code><\/pre>\n\n<p>Within my form builder function:<\/p>\n\n<pre><code class=\"language-php\">$form['date'] = array(\n '#title' => t('Arrival date'),\n\n \/\/ Provided by the date_popup module\n '#type' => 'date_popup',\n\n \/\/ Uses the PHP date() format - http:\/\/php.net\/manual\/en\/function.date.php\n '#date_format' => 'j F Y',\n\n \/\/ Limits the year range to the next two upcoming years\n '#date_year_range' => '0:+2',\n\n \/\/ Default value must be in 'Y-m-d' format.\n '#default_value' => date('Y-m-d', time()),\n);\n<\/code><\/pre>\n",
|
||
"tags": ["forms"
|
||
,"form-api"
|
||
,"date"
|
||
,"calendar"
|
||
,"drupal-7"
|
||
,"drupal-planet"
|
||
,"drupal"
|
||
]
|
||
}, {
|
||
"title": "Forward one domain to another using mod_rewrite and .htaccess",
|
||
"path": "/articles/forward-one-domain-another-using-modrewrite-and-htaccess",
|
||
"is_draft": "false",
|
||
"created": "1337731200",
|
||
"excerpt": "How to use the .htaccess file to forward to a different domain.",
|
||
"body": "<p>How to use the .htaccess file to forward to a different domain.<\/p>\n\n<p>Within the mod_rewrite section of your .htaccess file, add the following lines:<\/p>\n\n<pre><code class=\"language-apacheconf\">RewriteCond %{HTTP_HOST} ^yoursite\\.co\\.uk$\nRewriteRule (.*) http:\/\/yoursite.com\/$1 [R=301,L]\n<\/code><\/pre>\n\n<p>This automatically forwards any users from http:\/\/yoursite.co.uk to http:\/\/yoursite.com. This can also be used to forward multiple domains:<\/p>\n\n<pre><code class=\"language-apacheconf\">RewriteCond %{HTTP_HOST} ^yoursite\\.co\\.uk$ [OR]\nRewriteCond %{HTTP_HOST} ^yoursite\\.info$ [OR]\nRewriteCond %{HTTP_HOST} ^yoursite\\.biz$ [OR]\nRewriteCond %{HTTP_HOST} ^yoursite\\.eu$\nRewriteRule (.*) http:\/\/yoursite.com\/$1 [R=301,L]\n<\/code><\/pre>\n\n<p>If any of the RewriteCond conditions apply, then the RewriteRule is executed.<\/p>\n",
|
||
"tags": [".htaccess"
|
||
,"code"
|
||
,"drupal"
|
||
,"apache"
|
||
,"mod_rewrite"
|
||
]
|
||
}, {
|
||
"title": "Checkout a specific revision from SVN from the command line",
|
||
"path": "/articles/checkout-specific-revision-svn-command-line",
|
||
"is_draft": "false",
|
||
"created": "1337731200",
|
||
"excerpt": "How to checkout a specific revision from a SVN (Subversion) repository.",
|
||
"body": "<p>How to checkout a specific revision from a SVN (Subversion) repository.<\/p>\n\n<p>If you're checking out the repository for the first time:<\/p>\n\n<pre><code class=\"language-bash\">$ svn checkout -r 1234 url:\/\/repository\/path\n<\/code><\/pre>\n\n<p>If you already have the repository checked out:<\/p>\n\n<pre><code class=\"language-bash\">$ svn up -r 1234\n<\/code><\/pre>\n",
|
||
"tags": ["svn"
|
||
,"version-control"
|
||
]
|
||
}, {
|
||
"title": "Adding Custom Theme Templates in Drupal 7",
|
||
"path": "/articles/adding-custom-theme-templates-drupal-7",
|
||
"is_draft": "false",
|
||
"created": "1334793600",
|
||
"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.\n",
|
||
"body": "<p>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.<\/p>\n\n<p>I have previously seen it done this way by adding this into in a node.tpl.php file:<\/p>\n\n<pre><code class=\"language-php\">if ($teaser) {\n \/\/ The teaser output.\n}\nelse {\n \/\/ The whole node output.\n}\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>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 <code>debug()<\/code> function, I can easily see what's already there.<\/p>\n\n<pre><code class=\"language-php\">array (\n 0 => 'node__article',\n 1 => 'node__343',\n 2 => 'node__view__latest_news',\n 3 => 'node__view__latest_news__page',\n)\n<\/code><\/pre>\n\n<p>So, within my theme's template.php file:<\/p>\n\n<pre><code class=\"language-php\">\/**\n * Implementation of hook_preprocess_HOOK().\n *\/\nfunction mytheme_preprocess_node(&$variables) {\n $node = $variables['node'];\n\n if ($variables['teaser']) {\n \/\/ Add a new item into the theme_hook_suggestions array.\n $variables['theme_hook_suggestions'][] = 'node__' . $node->type . '_teaser';\n }\n}\n<\/code><\/pre>\n\n<p>After adding the new suggestion:<\/p>\n\n<pre><code class=\"language-php\">array (\n 0 => 'node__article',\n 1 => 'node__343',\n 2 => 'node__view__latest_news',\n 3 => 'node__view__latest_news__page',\n 4 => 'node__article_teaser',\n)\n<\/code><\/pre>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal"
|
||
]
|
||
}, {
|
||
"title": "Installing Nagios on CentOS",
|
||
"path": "/articles/installing-nagios-centos",
|
||
"is_draft": "false",
|
||
"created": "1334620800",
|
||
"excerpt": "How to install Nagios on CentOS.",
|
||
"body": "<p>A great post details that details the steps needed to install <a href=\"http:\/\/nagios.org\">Nagios<\/a> - a popular open source system and network monitoring software application - on CentOS.<\/p>\n\n<p><a href=\"http:\/\/saylinux.net\/story\/009506\/how-install-nagios-centos-55\">http:\/\/saylinux.net\/story\/009506\/how-install-nagios-centos-55<\/a><\/p>\n",
|
||
"tags": ["nagios"
|
||
,"centos"
|
||
,"linux"
|
||
]
|
||
}, {
|
||
"title": "Create an Omega Subtheme with LESS CSS Preprocessor using Omega Tools and Drush",
|
||
"path": "/articles/create-omega-subtheme-less-css-preprocessor-using-omega-tools-and-drush",
|
||
"is_draft": "false",
|
||
"created": "1334534400",
|
||
"excerpt": "How to create an Omega subtheme on the command line using Drush.",
|
||
"body": "<p>In this tutorial I'll be showing how to create an <a href=\"http:\/\/drupal.org\/project\/omega\">Omega<\/a> subtheme using the <a href=\"http:\/\/drupal.org\/project\/omega_tools\">Omega Tools<\/a> module, and have it working with the <a href=\"http:\/\/lesscss.org\">LESS CSS preprocessor<\/a>.<\/p>\n\n<p>The first thing that I need to do is download the Omega theme and the Omega Tools and <a href=\"http:\/\/drupal.org\/project\/less\" title=\"LESS module on drupal.org\">LESS<\/a> 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.<\/p>\n\n<pre><code class=\"language-bash\">$ drush dl less omega omega_tools;\n$ drush en -y less omega_tools\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ drush omega-subtheme \"Oliver Davies\" --machine_name=\"oliverdavies\" --enable --set-default\n<\/code><\/pre>\n\n<p>By default, four stylesheets are created within the subtheme's css directory. The first thing that I'm going to do is rename <code>global.css<\/code> to <code>global.less<\/code>.<\/p>\n\n<pre><code class=\"language-bash\">$ mv css\/global.css css\/global.less\n<\/code><\/pre>\n\n<p>Now I need to find all references to global.css within my oliverdavies.info file. I did this using <code>$ nano oliverdavies.info<\/code>, pressing <code>Ctrl+W<\/code> to search, then <code>Ctrl+R<\/code> to replace, entering <code>global.css<\/code> as the search phrase, and then <code>global.less<\/code> as the replacement text. After making any changes to oliverdavies.info, I need to clear Drupal's caches for the changes to be applied.<\/p>\n\n<pre><code class=\"language-bash\">$ drush cc all\n<\/code><\/pre>\n\n<p>I tested my changes by making some quick additions to my global.less file and reloading the page.<\/p>\n\n<p>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 <em>Layout configuration<\/em> and finding global.less at the bottom of <em>Enable optional stylesheets<\/em>.<\/p>\n",
|
||
"tags": ["drupal-7"
|
||
,"omega"
|
||
,"theming"
|
||
,"less"
|
||
,"drupal-planet"
|
||
,"drupal"
|
||
]
|
||
}, {
|
||
"title": "How to use Authorized Keys to Create a Passwordless SSH Connection",
|
||
"path": "/articles/use-authorized-keys-create-passwordless-ssh-connection",
|
||
"is_draft": "false",
|
||
"created": "1328054400",
|
||
"excerpt": "How to generate a SSH key, and how to use to log in to a server using SSH without entering a password.",
|
||
"body": "<p>If you're accessing Linux servers or automating tasks between servers, rather than having to enter your user password every time, you can also use SSH public key authentication. This is a simple process that involves creating a local key and storing it within the <em>authorized_keys<\/em> file on the remote server.<\/p>\n\n<ol>\n<li>Check if you already have a SSH key.\n<code>$ ssh-add -L<\/code><\/li>\n<li>If you don't have one, create one.\n<code>$ ssh-keygen<\/code><\/li>\n<li>Upload the key onto the server. Replace <em>myserver<\/em> with the hostname or IP address of your remote server.\n<code>$ ssh-copy-id myserver<\/code><\/li>\n<\/ol>\n\n<p>If you're using Mac OS X and you don't have ssh-copy-id installed, download and install <a href=\"http:\/\/mxcl.github.com\/homebrew\" title=\"Homebrew\">Homebrew<\/a> and run the <code>brew install ssh-copy-id<\/code> command.<\/p>\n\n<p>If successful, you should now see a message like:<\/p>\n\n<blockquote>\n <p>Now try logging into the machine, with \"ssh 'myserver'\", and check in:<\/p>\n \n <p>~\/.ssh\/authorized_keys<\/p>\n \n <p>to make sure we haven't added extra keys that you weren't expecting.<\/p>\n<\/blockquote>\n\n<p>Now the next time that you SSH onto the server, it should log you in without prompting you for your password.<\/p>\n",
|
||
"tags": ["linux"
|
||
,"ssh"
|
||
]
|
||
}, {
|
||
"title": "Site Upgraded to Drupal 7",
|
||
"path": "/articles/site-upgraded-drupal-7",
|
||
"is_draft": "false",
|
||
"created": "1325635200",
|
||
"excerpt": "As the vast majority of the Drupal websites that I currently work on are built on Drupal 7, I thought that it was time that I upgraded this site.",
|
||
"body": "<p>As the vast majority of the Drupal websites that I currently work on are built on Drupal 7, I thought that it was time that I upgraded this site. Following the <a href=\"http:\/\/drupal.org\/node\/570162\">core upgrade process<\/a> and the <a href=\"http:\/\/drupal.org\/node\/1144136\">CCK migration process<\/a>, everything was upgraded smoothly without any issues.<\/p>\n\n<p>I've upgraded a handful of essential contrib modules to the latest stable version, <a href=\"http:\/\/drupal.org\/project\/admin_menu\">Administration Menu<\/a>, <a href=\"http:\/\/drupal.org\/project\/views\">Views<\/a> etc., and will continue upgrading the other modules on the site as time allows.<\/p>\n\n<p>I also prefer <a href=\"http:\/\/drupal.org\/project\/bartik\">Bartik<\/a> to <a href=\"http:\/\/drupal.org\/project\/garland\">Garland<\/a> - but I will be creating a new custom theme when I get a chance.<\/p>\n",
|
||
"tags": ["drupal"
|
||
]
|
||
}, {
|
||
"title": "How to Install and Configure Subversion (SVN) Server on Ubuntu",
|
||
"path": "/articles/install-and-configure-subversion-svn-server-ubuntu",
|
||
"is_draft": "false",
|
||
"created": "1318982400",
|
||
"excerpt": "How to install and configure your own SVN server.",
|
||
"body": "<p>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.<\/p>\n\n<p>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.<\/p>\n\n<p>Firstly, I'm going to ensure that all of my installed packages are up to date, and install any available updates.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo apt-get update\n<\/code><\/pre>\n\n<p>Now, I need to download the subversion, subversion-tools and libapache2 packages.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo apt-get install subversion subversion-tools libapache2-svn\n<\/code><\/pre>\n\n<p>These are all of the packages that are needed to run a Subversion server.<\/p>\n\n<h2 id=\"create-subversion-directory\">Create subversion directory<\/h2>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo mkdir \/home\/svn\n<\/code><\/pre>\n\n<h2 id=\"create-a-test-repository\">Create a test repository<\/h2>\n\n<p>First, I'll create a new folder in which I'll create my test project, and then I'll create a repository for it.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo mkdir ~\/test\n$ sudo svnadmin create \/home\/svn\/test -m 'initial project structure'\n<\/code><\/pre>\n\n<p>This will create a new repository containing the base file structure.<\/p>\n\n<h2 id=\"adding-files-into-the-test-project\">Adding files into the test project<\/h2>\n\n<pre><code class=\"language-bash\">$ cd ~\/test\u2028\n$ mkdir trunk tags branches\n<\/code><\/pre>\n\n<p>I can now import these new directories into the test repository.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo svn import ~\/test file:\/\/\/home\/svn\/test -m 'Initial project directories'\n<\/code><\/pre>\n\n<p>This both adds and commits these new directories into the repository.<\/p>\n\n<p>In order for Apache to access the SVN repositories, the <code>\/home\/svn<\/code> 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.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo chown -R www-data:www-data \/home\/svn\n<\/code><\/pre>\n\n<h2 id=\"configuring-apache\">Configuring Apache<\/h2>\n\n<p>The first thing that I need to do is enable the dav_svn Apache module, using the a2enmod command.<\/p>\n\n<pre><code class=\"language-bash\">$ sudo a2enmod dav_svn\n<\/code><\/pre>\n\n<p>With this enabled, now I need to modify the Apache configuration file.<\/p>\n\n<pre><code class=\"language-bash\">$ cd \/etc\/apache2\n$ sudo nano apache2.conf\n<\/code><\/pre>\n\n<p>At the bottom of the file, add the following lines, and then save the file by pressing Ctrl+X.<\/p>\n\n<pre><code class=\"language-apacheconf\"><Location \"\/svn\">\n DAV svn\n SVNParentPath \/home\/svn\n<\/Location>\n<\/code><\/pre>\n\n<p>With this saved, restart the Apache service for the changes to be applied.<\/p>\n\n<pre><code class=\"language-bash\">sudo service apache2 restart\n<\/code><\/pre>\n\n<p>I can now browse through my test repository by opening Firefox, and navigating to <code>http:\/\/127.0.0.1\/svn\/test<\/code>. Here, I can now see my three directories, although they are currently all empty.<\/p>\n\n<h2 id=\"securing-my-svn-repositories\">Securing my SVN repositories<\/h2>\n\n<p>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.<\/p>\n\n<p>Re-open apache2.conf, and replace the SVN Location information with this:<\/p>\n\n<pre><code class=\"language-apacheconf\"><Location \"\/svn\">\n DAV svn\n SVNParentPath \/home\/svn\n AuthType Basic\n AuthName \"My SVN Repositories\"\n AuthUserFile \/etc\/svn-auth\n Require valid-user\n<\/Location>\n<\/code><\/pre>\n\n<p>Now I need to create the password file.<\/p>\n\n<pre><code class=\"language-bash\">$ htpasswd -cm \/etc\/svn-auth oliver\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<h2 id=\"checking-out-the-repository-and-commiting-files\">Checking out the repository and commiting files<\/h2>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ cd ~\n$ mkdir test2\n$ svn checkout http:\/\/127.0.0.1\/svn\/test\/trunk test2\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">$ svn add index.php\n$ svn add *\n<\/code><\/pre>\n\n<p>With all the required files added, they can be committed using <code>svn commit -m 'commit message'<\/code> command, and the server can be updated using the svn up command.<\/p>\n",
|
||
"tags": ["svn"
|
||
,"ubuntu"
|
||
,"version-control"
|
||
]
|
||
}, {
|
||
"title": "Create Multigroups in Drupal 7 using Field Collections",
|
||
"path": "/articles/create-multigroups-drupal-7-using-field-collections",
|
||
"is_draft": "false",
|
||
"created": "1314489600",
|
||
"excerpt": "How to replicate CCK\u2019s multigroups in Drupal 7 using the Field Collections module.",
|
||
"body": "<p>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.<\/p>\n\n<p>But at the time of writing this, there is no CCK 3 version for Drupal 7. So, I created the same thing using <a href=\"http:\/\/drupal.org\/project\/field_collection\">Field Collection<\/a> and <a href=\"http:\/\/drupal.org\/project\/entity\">Entity<\/a> modules.<\/p>\n\n<p>With the modules uploaded and enabled, go to admin\/structure\/field-collections and create a field collection.<\/p>\n\n<p>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'.<\/p>\n\n<p>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.<\/p>\n\n<p>With the Field Collection created, I can now add it as a field within my content type.<\/p>\n\n<p>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.<\/p>\n\n<p>After some searching I found <a href=\"http:\/\/drupal.org\/node\/977890#comment-4184524\">a link to a patch<\/a> 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.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-7"
|
||
,"drupal-planet"
|
||
,"cck"
|
||
,"fields"
|
||
,"field-collection"
|
||
,"entity-api"
|
||
,"multigroup"
|
||
]
|
||
}, {
|
||
"title": "Imagefield Import Archive",
|
||
"path": "/articles/imagefield-import-archive",
|
||
"is_draft": "false",
|
||
"created": "1306108800",
|
||
"excerpt": "I've finally uploaded my first module onto Drupal.org!",
|
||
"body": "<p>I've finally uploaded my first module onto Drupal.org!<\/p>\n\n<p>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 <a href=\"http:\/\/drupal.org\/project\/imagefield_import\">Imagefield Import<\/a> 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.<\/p>\n\n<p>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.<\/p>\n\n<p>The module is currently a <a href=\"http:\/\/drupal.org\/sandbox\/opdavies\/1165110\">sandbox project<\/a> on Drupal.org, although I have applied for full project access so that I can be added as a fully-fledged downloadable module.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"imagefield-import"
|
||
]
|
||
}, {
|
||
"title": "Proctors Hosting the next Drupal Meetup",
|
||
"path": "/articles/proctors-hosting-next-drupal-meetup",
|
||
"is_draft": "false",
|
||
"created": "1305849600",
|
||
"excerpt": "Proctor & Stevenson are going to be hosting the next Bristol & South West Drupal meetup.",
|
||
"body": "<p>My employer, <a href=\"http:\/\/www.proctors.co.uk\">Proctor & Stevenson<\/a>, are going to be hosting the next Bristol & South West Drupal meetup on the 25th May at our offices.<\/p>\n\n<p>You can <a href=\"http:\/\/groups.drupal.org\/node\/147324\">view more details<\/a>, or register <a href=\"http:\/\/www.proctors.co.uk\/Drupal-SWUG-Meetup\">on our website<\/a>.<\/p>\n",
|
||
"tags": ["meetups"
|
||
,"drupal-bristol"
|
||
]
|
||
}, {
|
||
"title": "Proctor & Stevenson",
|
||
"path": "/articles/proctor-stevenson",
|
||
"is_draft": "false",
|
||
"created": "1301529600",
|
||
"excerpt": "I\u2019m moving jobs.",
|
||
"body": "<p>2 weeks ago, I handed in my notice of resignation to <a href=\"http:\/\/horseandcountry.tv\">Horse & Country TV<\/a>because I've been offered a new role at <a href=\"http:\/\/proctors.co.uk\">Proctor & Stevenson<\/a> - a Marketing Design and Communications agency in Bristol.<\/p>\n\n<p>Proctors have an <a href=\"http:\/\/www.proctors.co.uk\/clients\">extensive client list<\/a> - including <a href=\"http:\/\/www.proctors.co.uk\/clients\/bmw-financial-services\">BMW<\/a>, <a href=\"http:\/\/www.proctors.co.uk\/clients\/panasonic\">Panasonic<\/a>, the <a href=\"http:\/\/www.proctors.co.uk\/clients\/open-university\">Open University<\/a> and <a href=\"http:\/\/www.proctors.co.uk\/clients\/vosa\">VOSA<\/a>, and it's going to be a fantastic opportunity for me to continue expanding my skillset whilst gaining vital experience.<\/p>\n",
|
||
"tags": ["personal"
|
||
]
|
||
}, {
|
||
"title": "Display the Number of Facebook fans in PHP",
|
||
"path": "/articles/display-number-facebook-fans-php",
|
||
"is_draft": "false",
|
||
"created": "1300147200",
|
||
"excerpt": "How to use PHP to display the number of fans of a Facebook page.",
|
||
"body": "<p>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.<\/p>\n\n<p>For example, mine is <a href=\"https:\/\/www.facebook.com\/pages\/edit\/?id=143394365692197&sk=basic\">https:\/\/www.facebook.com\/pages\/edit\/?id=143394365692197&sk=basic<\/a>.<\/p>\n\n<p>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 <a href=\"http:\/\/www.horseandcountry.tv\/events\/paid\">Gold Event listing<\/a> on the Horse & Country TV website.<\/p>\n\n<pre><code class=\"language-php\">$page_id = \"143394365692197\";\n$xml = @simplexml_load_file(\"http:\/\/api.facebook.com\/restserver.php?method=facebook.fql.query&amp;query=SELECT%20fan_count%20FROM%20page%20WHERE%20page_id=\".$page_id.\"\") or die (\"a lot\");\n$fans = $xml->page->fan_count;\nprint number_format($fans);\n<\/code><\/pre>\n\n<p>This code was originally found at <a href=\"http:\/\/wp-snippets.com\/display-number-facebook-fans\">http:\/\/wp-snippets.com\/display-number-facebook-fans<\/a>.<\/p>\n",
|
||
"tags": ["php"
|
||
]
|
||
}, {
|
||
"title": "Easily Embed TypeKit Fonts into your Drupal Website",
|
||
"path": "/articles/easily-embed-typekit-fonts-your-drupal-website",
|
||
"is_draft": "false",
|
||
"created": "1297641600",
|
||
"excerpt": "How to use the @font-your-face module to embed TypeKit fonts into your Drupal website.",
|
||
"body": "<p>To begin with, you will need to <a href=\"https:\/\/typekit.com\/plans\">register for a TypeKit account<\/a> - there is a free version if you just want to try it out.<\/p>\n\n<p>Next, you'll need to create a kit that contains the fonts that you want to use on your website. I've used <a href=\"http:\/\/typekit.com\/fonts\/ff-tisa-web-pro\">FF Tisa Web Pro<\/a>.<\/p>\n\n<p>Under 'Kit Settings', ensure that your website domain (e.g. mysite.com) is listed under 'Domains'.<\/p>\n\n<p>Download and install the <a href=\"http:\/\/drupal.org\/project\/fontyourface\">@font-your-face<\/a> module onto your Drupal site, and to go admin\/settings\/fontyourface to configure it. Enter <a href=\"https:\/\/typekit.com\/account\/tokens\">your TypeKit API key<\/a>, and click 'Import Typekit' to import your kits and fonts.<\/p>\n\n<p>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.<\/p>\n\n<p>With the font enabled, you can now use it in your CSS.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"typekit"
|
||
]
|
||
}, {
|
||
"title": "Use Regular Expressions to Search and Replace in Coda or TextMate",
|
||
"path": "/articles/use-regular-expressions-search-and-replace-coda-or-textmate",
|
||
"is_draft": "false",
|
||
"created": "1288828800",
|
||
"excerpt": "How to perform searches using regular expressions.",
|
||
"body": "<p>As in <a href=\"\/blog\/add-taxonomy-term-multiple-nodes-using-sql\/\" title=\"Quickly adding a taxonomy term to multiple nodes using SQL\">the original post<\/a>, I'd generated a list of node ID values, and needed to add structure the SQL update statment formatted in a certain way. However, I changed my inital query slightly to out put the same nid value twice.<\/p>\n\n<pre><code class=\"language-sql\">SELECT nid, nid FROM node WHERE TYPE = 'blog' ORDER BY nid ASC;\n<\/code><\/pre>\n\n<p>Then, I could select all of the returned rows, copy the values, and paste them into Coda:<\/p>\n\n<p>As before, I needed my SQL update statement to be in the following format:<\/p>\n\n<pre><code class=\"language-sql\">INSERT INTO term_node VALUE (nid, vid, tid), (nid2, vid2, tid);\n<\/code><\/pre>\n\n<p>As I mentioned previously, the nid and vid values are the same for each node, and the tid will remain constant. In this case, the tid value that I needed to use was '63'.<\/p>\n\n<p>So, using the 'Find and Replace' function within Coda, combined with <a href=\"http:\/\/en.wikipedia.org\/wiki\/Regular_expression\">regular expressions<\/a> (regex), I can easily format the values as needed. To begin with, I need to ensure that the RegEx search option is enabled, and that I'm using the correct escape character.<\/p>\n\n<p>The first thing that I wanted to do was add the seperating comma between the two values. To do this, I perform a search for <code>\\s*\\t<\/code>. This searches for everything that is whitespace AND is a tab value. I can then add the comma as the replacement for each result.<\/p>\n\n<p>All 31 lines have been changed.<\/p>\n\n<p>Next, I can use <code>\\n<\/code> to target the lines between the rows. I'll replace it with the next comma, the number 63 (the tid value), the closing bracket, another comma, re-add the line and add the opening bracket.<\/p>\n\n<p>The only two lines that aren't changed are the first and last, as they don't have any line breaks following them. I can complete these lines manually. Now all I need to do is add the beginning of the SQL update statement, then copy and paste it into Sequel Pro.<\/p>\n",
|
||
"tags": ["taxonomy"
|
||
,"sequel-pro"
|
||
,"database"
|
||
,"coda"
|
||
,"regular-expression"
|
||
,"textmate"
|
||
]
|
||
}, {
|
||
"title": "Create a Better Photo Gallery in Drupal - Part 2.1",
|
||
"path": "/articles/create-better-photo-gallery-drupal-part-21",
|
||
"is_draft": "false",
|
||
"created": "1287705600",
|
||
"excerpt": "The missing code to get totals of galleries and photos.",
|
||
"body": "<p>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 <a href=\"\/blog\/create-better-photo-gallery-drupal-part-2\/\" title=\"Create a Better Photo Gallery in Drupal - Part 2\">Part 2<\/a> that I'd include it in <a href=\"\/blog\/create-better-photo-gallery-drupal-part-3\/\" title=\"Create a Better Photo Gallery in Drupal - Part 3\">Part 3<\/a>, but I forgot). So, here it is:<\/p>\n\n<pre><code class=\"language-php\"><?php\n\n\/\/ Queries the database and returns a list of nids of published galleries.\n$galleries = db_query(\"SELECT nid FROM {node} WHERE type = 'gallery' AND status = 1\");\n\/\/ Resets the number of photos.\n$output = 0;\n\/\/ Prints a list of nids of published galleries.\nwhile($gallery = db_fetch_array($galleries)) {\n $gallery_id = $gallery['nid'];\n $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\"));\n}\n\n\/\/ Prints the output.\nprint 'There ';\nif($photos == 1) {\n print 'is';\n} \nelse {\n print 'are';\n}\nprint ' currently ';\nprint $photos . ' ';\nif($photos == 1) {\n print 'photo';\n} \nelse {\n print 'photos';\n} \nprint ' in ';\n\n\/\/ Counts the number of published galleries on the site.\n$galleries = db_result(db_query(\"SELECT COUNT(*) FROM {node} WHERE TYPE = 'gallery' AND STATUS = 1\"));\n\n\/\/ Prints the number of published galleries.\nprint $galleries;\nif ($galleries == 1) {\n print ' gallery';\n} \nelse {\n print ' galleries';\n}\nprint '.';\n?>\n<\/code><\/pre>\n\n<p>It was applied to the view as a header which had the input format set to PHP code.<\/p>\n",
|
||
"tags": ["drupal"
|
||
]
|
||
}, {
|
||
"title": "Create a Better Photo Gallery in Drupal - Part 3",
|
||
"path": "/articles/create-better-photo-gallery-drupal-part-3",
|
||
"is_draft": "false",
|
||
"created": "1286928000",
|
||
"excerpt": "Grouping galleries by category.",
|
||
"body": "<p>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.<\/p>\n\n<p>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 <code>gallery\/%<\/code>. The other thing that I need to do is overwrite the output of the original 'All Galleries' View by creating a file called <code>views-view--photo-gallery--page-1.tpl.php<\/code> and placing it within my theme directory.<\/p>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-php\"><?php views_embed_view('my_view', 'block_1', $arg1, $arg2); ?>\n<\/code><\/pre>\n\n<p>So, to display the galleries that are assigned the taxonomy of 'tournaments', I can use the following:<\/p>\n\n<pre><code class=\"language-php\"><?php print views_embed_view('photo_gallery', 'page_2', 'tournaments'); ?>\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\"><?php\n$terms = db_query(\"SELECT * FROM {term_data} WHERE vid = 1\");\nwhile ($term = db_fetch_array($terms)) {\n print '<h3>' . $term['name'] . '<\/h3>';\n print views_embed_view('gallery', 'page_2', $term['name']);\n}\n?>\n<\/code><\/pre>\n",
|
||
"tags": ["drupal"
|
||
]
|
||
}, {
|
||
"title": "How to Create and Apply Patches",
|
||
"path": "/articles/create-and-apply-patches",
|
||
"is_draft": "false",
|
||
"created": "1286668800",
|
||
"excerpt": "How to create and apply patches, ready for the Drupal.org issue queues.",
|
||
"body": "<p>Earlier this year, I posted a solution to <a href=\"http:\/\/drupal.org\/node\/753898\">an issue<\/a> 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 <code>user_pass_validate()<\/code> function that's found within <code>modules\/user\/user.pages.inc<\/code>.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-bash\">diff -rup modules\/user\/user.pages.inc modules\/user\/user.pages2.inc > \/Users\/oliver\/Desktop\/different_messages_for_blocked_users.patch\n<\/code><\/pre>\n\n<p>This command compares the differences between the two files, and creates the specified patch file.<\/p>\n\n<p>To apply the patch to my Drupal installation, I go back to Terminal and run the following code:<\/p>\n\n<pre><code class=\"language-bash\">patch -p0 < \/Users\/oliver\/Desktop\/different_messages_for_blocked_users.patch\n<\/code><\/pre>\n\n<p>If, for some reason, I need to reverse the patch, I can run this code:<\/p>\n\n<pre><code class=\"language-bash\">patch -p0 -R < \/Users\/oliver\/Desktop\/different_messages_for_blocked_users.patch\n<\/code><\/pre>\n\n<p>And that's it!<\/p>\n\n<p>There is also a Git patch creation workflow, which is described at <a href=\"http:\/\/groups.drupal.org\/node\/91424\">http:\/\/groups.drupal.org\/node\/91424<\/a>. Thanks to <a href=\"http:\/\/randyfay.com\">Randy Fay<\/a> for making me aware of this, and suggesting a slight change to my original patch creation command.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"modules"
|
||
,"patches"
|
||
]
|
||
}, {
|
||
"title": "The Inaugural Meetup for the South Wales Drupal User Group",
|
||
"path": "/articles/south-wales-drupal-user-group",
|
||
"is_draft": "false",
|
||
"created": "1285459200",
|
||
"excerpt": "If you do Drupal and you're in the area, come and join us for the first SWDUG meetup!",
|
||
"body": "<p>If you do Drupal and you're in the area, come and join us for the first SWDUG meetup!<\/p>\n\n<p>We'll be meeting in the communal area just outside of the <a href=\"http:\/\/www.subhub.com\">SubHub<\/a> HQ, at:<\/p>\n\n<p>4, The Studios<br>\n3 Burt Street<br>\nCardiff<br>\nCF10 5FZ<\/p>\n\n<p>For more information and to signup, visit <a href=\"http:\/\/groups.drupal.org\/node\/95104\">http:\/\/groups.drupal.org\/node\/95104<\/a>.<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-planet"
|
||
,"swdug"
|
||
,"meetups"
|
||
]
|
||
}, {
|
||
"title": "Review of the Image Caption Module",
|
||
"path": "/articles/review-image-caption-module",
|
||
"is_draft": "false",
|
||
"created": "1282262400",
|
||
"excerpt": "My review of Drupal\u2019s Image Caption module.",
|
||
"body": "<p>Up until as recent as last week, whenever I added an image into one of my Blog posts, I was manually adding the caption below each image and styling it accordingly. That was until I installed the <a href=\"http:\/\/drupal.org\/project\/image_caption\">Image Caption<\/a> module.<\/p>\n\n<p>The Image Caption module uses jQuery to dynamically add captions to images. Here is a walkthrough of the process that I followed to install and configure the module. As always, I used Drush to download and enable the module, then visited the Image Caption Settings page (admin\/settings\/image_caption). Here, I select which node types should be included in image captioning. In my case, I only wanted this to apply to Blog posts.<\/p>\n\n<p>As I use the <a href=\"http:\/\/drupal.org\/project\/filefield\">FileField<\/a>, <a href=\"http:\/\/drupal.org\/project\/imagefield\">ImageField<\/a> and <a href=\"http:\/\/drupal.org\/project\/insert\">Insert<\/a> modules to add images to my posts, as opposed to via a WYSIWYG editor, I'm able to add the CSS class of 'caption' to my images.<\/p>\n\n<p>Now, all images inserted this way will have the CSS class of 'caption'.<\/p>\n\n<p>As the Image Caption module uses the image's title tag to create the displayed caption, I enabled the custom title text for my Image field so that when I upload an image, I'm prompted to enter text for the caption.<\/p>\n\n<p>This results in a span called <code>image-caption-container<\/code> around the inserted image, and a caption below it called <code>image-caption<\/code> containing the text.<\/p>\n\n<p>All that's left is to style these classes within your CSS stylesheet.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal"
|
||
,"drupal-6"
|
||
,"imagefield"
|
||
,"image-caption"
|
||
]
|
||
}, {
|
||
"title": "Create a Better Photo Gallery in Drupal - Part 2",
|
||
"path": "/articles/create-better-photo-gallery-drupal-part-2",
|
||
"is_draft": "false",
|
||
"created": "1282003200",
|
||
"excerpt": "Updating the galleries\u2019 created and modified dates.",
|
||
"body": "<p>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.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-sql\">SELECT n.title, n.nid, n.created, n.changed, p.field_gallery_nid\nFROM node n, content_type_photo pWHERE n.type = 'photo'\nAND p.field_gallery_nid = 103AND n.nid = p.nid\nORDER BY n.nid ASC;\n<\/code><\/pre>\n\n<p>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 <a href=\"http:\/\/www.onlineconversion.com\/unix_time.htm\">http:\/\/www.onlineconversion.com\/unix_time.htm<\/a>, 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.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-sql\">UPDATE node\nINNER JOIN content_type_photo\nON node.nid = content_type_photo.nid\nSET\n node.created = 1217149200,\n node.changed = 1217149200\nWHERE content_type_photo.field_gallery_nid = 103\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>Once the changes have been applied, it's a case of repeating the above process for each of the required galleries.<\/p>\n\n<p>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 <a href=\"http:\/\/drupal.org\/project\/shadowbox\">Shadowbox<\/a> module.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"photo-gallery"
|
||
,"sql"
|
||
,"sequel-pro"
|
||
]
|
||
}, {
|
||
"title": "Create a Better Photo Gallery in Drupal - Part 1",
|
||
"path": "/articles/create-better-photo-gallery-drupal-part-1",
|
||
"is_draft": "false",
|
||
"created": "1281484800",
|
||
"excerpt": "How I started converting and migrating a Coppermine photo gallery into Drupal.",
|
||
"body": "<p>Recently, I converted a client's static HTML website, along with their Coppermine Photo Gallery, into a Drupal-powered website.<\/p>\n\n<p>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.<\/p>\n\n<p>To begin with, I created my photo gallery as described by <a href=\"http:\/\/www.lullabot.com\/about\/team\/jeff-eaton\">Jeff Eaton<\/a> in <a href=\"http:\/\/www.lullabot.com\/articles\/photo-galleries-views-attach\">this screencast<\/a>, downloaded all my client's previous photos via FTP, and quickly added them into the new gallery using the <a href=\"http:\/\/drupal.org\/project\/imagefield_import\">Imagefield Import<\/a> module (which I mentioned <a href=\"\/blog\/quickly-import-multiples-images-using-imagefieldimport-module\/\">previously<\/a>).<\/p>\n\n<p>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.<\/p>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-sql\">SELECT title \nAS title, nid \nAS gallery_idFROM node\nWHERE type = 'gallery'\nAND status = 1;\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>For example, using <a href=\"http:\/\/www.w3schools.com\/sql\/sql_alias.asp\">aliasing<\/a> 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:<\/p>\n\n<pre><code class=\"language-sql\">SELECT n.title, n.nid, p.field_gallery_nid\nFROM node n, content_type_photo p\nWHERE p.field_gallery_nid = 105\nAND n.status = 1\nAND n.nid = p.nid;\n<\/code><\/pre>\n\n<p>I can easily change this to count the number of published nodes by changing the first line of the query to read SELECT COUNT(*).<\/p>\n\n<pre><code class=\"language-sql\">SELECT COUNT(*)\nFROM node n, content_type_photo p\nWHERE p.field_gallery_nid = 105\nAND n.status = 1\nAND n.nid = p.nid;\n<\/code><\/pre>\n\n<p>As I've used the <a href=\"http:\/\/drupal.org\/project\/views_attach\">Views Attach<\/a> 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:<\/p>\n\n<pre><code class=\"language-php\"><?php\n$selected_gallery = db_result(db_query(\"\nSELECT nid \nFROM {node} \nWHERE type = 'gallery' \nAND title = '$title'\n\"));\n?>\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\"><?php\n$gallery_total = db_result(db_query(\"\nSELECT COUNT(*) \nFROM {content_type_photo} \nWHERE field_gallery_nid = $selected_gallery\n\"));\n?>\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\"><?php\n$latest_photo = db_result(db_query(\"\nSELECT n.created \nFROM {node} n, {content_type_photo} p \nWHERE p.field_gallery_nid = $selected_gallery \nAND n.nid = p.nid \nORDER BY n.created DESC LIMIT 1\n\"));\n?>\n<\/code><\/pre>\n\n<p>This was all then added into a 'print' statement which displayed it into the page.<\/p>\n\n<pre><code class=\"language-php\"><?php\nif ($selected_gallery_total != 0) {\n $output = '<i>There are currently ' . $selected_gallery_total . ' photos in this gallery.';\n $output .= 'Last one added on ' . $latest_photo . '<\/i>';\n print $output;\n}\n?>\n<\/code><\/pre>\n\n<p>OK, so let's take a look at the Gallery so far:<\/p>\n\n<p>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:<\/p>\n\n<pre><code class=\"language-php\"><?php\nif ($selected_gallery_total != 0) {\n $output = '<i>There are currently ' . $selected_gallery_total . ' photos in this gallery.';\n $output .= 'Last one added on ' . date(\"l, jS F, Y\", $latest_photo) . '.<\/i>';\n print $output;\n}\n?>\n<\/code><\/pre>\n\n<p>The values that I've entered are from <a href=\"http:\/\/php.net\/manual\/en\/function.date.php\">this page<\/a> on PHP.net, and can be changed according on how you want the date to be displayed.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"photo-gallery"
|
||
,"sql"
|
||
,"views"
|
||
,"sequel-pro"
|
||
,"cck"
|
||
,"views-attach"
|
||
,"drupal"
|
||
]
|
||
}, {
|
||
"title": "Review of the Admin:hover Module",
|
||
"path": "/articles/review-adminhover-module",
|
||
"is_draft": "false",
|
||
"created": "1281398400",
|
||
"excerpt": "My review of Drupal\u2019s admin:hover module.",
|
||
"body": "<p>Sorry for the lack of Blog posts lately, but <a href=\"http:\/\/horseandcountry.tv\">my new job<\/a> that I started a few weeks ago has certainly been keeping me busy! I've got a few more posts that I'm preparing content for, and I'll hopefully be back into my weekly-post routine before too long!<\/p>\n\n<p>Today, I'd like to just give a quick overview of the <a href=\"http:\/\/drupal.org\/project\/admin_hover\">Admin:hover<\/a> module. It basically adds an administrative menu that pops up when you hover over a node or block within your Drupal website - the kind of functionality that was present within previous versions of the <a href=\"http:\/\/drupal.org\/project\/admin\">Admin module<\/a>. It also integrates well with the <a href=\"http:\/\/drupal.org\/project\/devel\">Devel<\/a> and <a href=\"http:\/\/drupal.org\/project\/node_clone\">Clone<\/a> modules.<\/p>\n\n<p>I've found this to be extremely useful whilst working on photo galleries etc. where multiple nodes are displayed in a grid format and I quickly need to publish or unpublish something for testing purposes. No longer do I need to open each node, or go into the administration area to perform the required actions.<\/p>\n\n<p>It is also possible to customise which links are available from within the adminstration area. The possible selections that I currently have on this site are as follows:<\/p>\n\n<p><strong>Node links:<\/strong><\/p>\n\n<ul>\n<li>Edit<\/li>\n<li>Publish<\/li>\n<li>Unpublish<\/li>\n<li>Promote<\/li>\n<li>Unpromote<\/li>\n<li>Make sticky<\/li>\n<li>Make unsticky<\/li>\n<li>Delete<\/li>\n<li>Clone<\/li>\n<li>Dev load<\/li>\n<li>View author<\/li>\n<li>Edit author<\/li>\n<li>Add<\/li>\n<\/ul>\n\n<p><strong>Block links:<\/strong><\/p>\n\n<ul>\n<li>Configure block<\/li>\n<li>Add block<\/li>\n<\/ul>\n\n<p>Although, as I have additional contributed modules installed, some of these may not neccassaily be available out of the box.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal-modules"
|
||
,"admin:hover"
|
||
,"administration"
|
||
]
|
||
}, {
|
||
"title": "Review of the Teleport Module",
|
||
"path": "/articles/review-teleport-module",
|
||
"is_draft": "false",
|
||
"created": "1278892800",
|
||
"excerpt": "My review of Drupal\u2019s Teleport module.",
|
||
"body": "<p>As a heavily-reliant <a href=\"http:\/\/en.wikipedia.org\/wiki\/Quicksilver_%28software%29\">Quicksilver<\/a> user on my MacBook Pro, I was glad when I found the <a href=\"http:\/\/drupal.org\/project\/teleport\">Teleport<\/a> module for <a href=\"http:\/\/drupal.org\">Drupal<\/a> <em>(due to Elliott Rothman's <a href=\"http:\/\/twitter.com\/elliotttt\/status\/18044234238\">tweet<\/a>)<\/em>.<\/p>\n\n<p>When you press a configurable hot-key, a jQuery dialog box appears where you can search for nodes by title or path, or directly enter the path that you want to navigate to. This will greatly reduce the number of clicks that I need to perform to get to my desired page - even compared to the <a href=\"http:\/\/drupal.org\/project\/admin\">Admin<\/a> and <a href=\"http:\/\/drupal.org\/project\/admin_menu\">Administration Menu<\/a> modules.<\/p>\n\n<p>Although it's not a new module (the first commits were 2 years ago), I hope that they are still planning on achieving the list of future directions listed on their Drupal.org project page:<\/p>\n\n<ul>\n<li>Make interface act more like Quicksilver (i.e. you should only have to press Enter once to launch)<\/li>\n<li>'Actions' like Quicksilver: if you select a node, a second input should appear with options to go to the View page, Edit page, (un)publish, etc. Same with users.<\/li>\n<li>Hook into more non-node content, like taxonomy terms and functions in the API module.<\/li>\n<\/ul>\n\n<p>Personally, this will make navigation around both the front-end and administration area of my Drupal sites so much easier.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal-modules"
|
||
,"teleport"
|
||
]
|
||
}, {
|
||
"title": "Add a Taxonomy Term to Multiple Nodes Using SQL",
|
||
"path": "/articles/add-taxonomy-term-multiple-nodes-using-sql",
|
||
"is_draft": "false",
|
||
"created": "1278460800",
|
||
"excerpt": "How to add a new taxonomy term to multiple nodes in Drupal using SQL.",
|
||
"body": "<p>In preparation for my Blog posts being added to <a href=\"http:\/\/drupal.org\/planet\">Drupal Planet<\/a>, 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.<\/p>\n\n<p>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 - <a href=\"\/blog\/change-content-type-multiple-nodes-using-sql\/\">Quickly Change the Content Type of Multiple Nodes using SQL<\/a>.<\/p>\n\n<p><strong>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!<\/strong><\/p>\n\n<p>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 <a href=\"http:\/\/www.sequelpro.com\">Sequel Pro<\/a>, I ran the following SQL query to give me a list of Blog posts on my site - showing just their titles and nid values.<\/p>\n\n<pre><code class=\"language-sql\">SELECT title, nid FROM node WHERE TYPE = 'blog' ORDER BY title ASC;\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-sql\">SELECT NAME, tid FROM term_data ORDER BY NAME ASC;\n<\/code><\/pre>\n\n<p>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 <a href=\"http:\/\/w3schools.com\/sql\/sql_alias.asp\">http:\/\/w3schools.com\/sql\/sql_alias.asp<\/a>.<\/p>\n\n<pre><code class=\"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;\n<\/code><\/pre>\n\n<p>As expected, it returned no rows.<\/p>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-sql\">SELECT nid, tid FROM term_node WHERE nid = 216;\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["taxonomy"
|
||
,"drupal-planet"
|
||
,"drupal-6"
|
||
,"sql"
|
||
,"sequal-pro"
|
||
,"database"
|
||
]
|
||
}, {
|
||
"title": "Create Virtual Hosts on Mac OS X Using VirtualHostX",
|
||
"path": "/articles/create-virtual-hosts-mac-os-x-using-virtualhostx",
|
||
"is_draft": "false",
|
||
"created": "1278028800",
|
||
"excerpt": "How to use the VirtualHostX application to manage virtual hosts on Mac OS X.",
|
||
"body": "<p>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 <a href=\"http:\/\/www.mamp.info\/en\/index.html\">MAMP<\/a> 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.<\/p>\n\n<p>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 <a href=\"http:\/\/clickontyler.com\/virtualhostx\">VirtualHostX<\/a>, 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.<\/p>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<p><strong>Note:<\/strong> 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 <em>'No such file or directory'.<\/em><\/p>\n\n<p>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!<\/p>\n\n<p>This, along with <a href=\"http:\/\/www.mamp.info\/en\/index.html\">MAMP<\/a>, <a href=\"http:\/\/www.panic.com\/coda\">Coda<\/a>, <a href=\"http:\/\/www.sequelpro.com\">Sequel Pro<\/a>, and <a href=\"http:\/\/www.panic.com\/transmit\">Transmit<\/a>, has become an essential tool within my development environment.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"mamp"
|
||
,"virtual-hosts"
|
||
,"virtualhostx"
|
||
]
|
||
}, {
|
||
"title": "Change the Content Type of Multiple Nodes Using SQL",
|
||
"path": "/articles/change-content-type-multiple-nodes-using-sql",
|
||
"is_draft": "false",
|
||
"created": "1277942400",
|
||
"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.",
|
||
"body": "<p>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'.<\/p>\n\n<p><strong>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!<\/strong><\/p>\n\n<p>To begin with, I created the 'News article' content type, and then used the Devel Generate module to generate some Blog nodes.<\/p>\n\n<p>Using <a href=\"http:\/\/www.sequelpro.com\">Sequel Pro<\/a>, I can query the database to view the Blog posts (you can also do this via the <a href=\"http:\/\/guides.macrumors.com\/Terminal\">Terminal<\/a> in a Mac OS X\/Linux, <a href=\"http:\/\/www.oracle.com\/technology\/software\/products\/sql\/index.html\">Oracle SQL Developer<\/a> on Windows, or directly within <a href=\"http:\/\/www.phpmyadmin.net\/home_page\/index.php\">phpMyAdmin<\/a>):<\/p>\n\n<p>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.<\/p>\n\n<p>Now, when I query the database, the type is shown as 'article'.<\/p>\n\n<p>Now, when I go back into the administration section of my site and view the content, the content type now shows at 'News article'.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal"
|
||
,"sql"
|
||
,"sequel-pro"
|
||
,"database"
|
||
,"content-types"
|
||
]
|
||
}, {
|
||
"title": "Create a Flickr Photo Gallery Using Feeds, CCK and Views",
|
||
"path": "/articles/create-flickr-photo-gallery-using-feeds-cck-and-views",
|
||
"is_draft": "false",
|
||
"created": "1277683200",
|
||
"excerpt": "In this tutorial, I'll show you how to create a photo gallery which uses photos imported from Flickr.",
|
||
"body": "<p>In this tutorial, I'll show you how to create a photo gallery which uses photos imported from <a href=\"http:\/\/www.flickr.com\">Flickr<\/a>.<\/p>\n\n<p>The modules that I'll use to create the Gallery are:<\/p>\n\n<ul>\n<li><a href=\"http:\/\/drupal.org\/project\/cck\">CCK<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/feeds\">Feeds<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/feeds_imagegrabber\">Feeds Image Grabber<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/filefield\">FileField<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/imageapi\">ImageAPI<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/imagecache\">ImageCache<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/imagefield\">ImageField<\/a><\/li>\n<li><a href=\"http:\/\/drupal.org\/project\/views\">Views<\/a><\/li>\n<\/ul>\n\n<p>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.<\/p>\n\n<p>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:<\/p>\n\n<ul>\n<li><strong>Basic settings:<\/strong> I changed the Refresh time to 15 minutes.<\/li>\n<li><strong>Processor settings:<\/strong> I changed the content type to 'Photo', and the author's name from 'anonymous'.<\/li>\n<li><strong>Processor mapping:<\/strong> I added a new mapping from 'Item URL (link)' to 'Photo (FIG)'. The Photo FIG target is added by the Image Grabber module.<\/li>\n<\/ul>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<p>With the new Photo nodes created, I then created the View to display them.<\/p>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"photo-gallery"
|
||
,"views"
|
||
,"cck"
|
||
,"imagecache"
|
||
,"feeds"
|
||
,"filefield"
|
||
,"flickr"
|
||
,"imagefield"
|
||
]
|
||
}, {
|
||
"title": "10 Useful Drupal 6 Modules",
|
||
"path": "/articles/useful-drupal-6-modules",
|
||
"is_draft": "false",
|
||
"created": "1277424000",
|
||
"excerpt": "A list of 10 contributed modules that I currently use on each Drupal project.",
|
||
"body": "<p>Aside from the obvious candidates such as Views, CCK etc, here are a list of 10 contributed modules that I currently use on each Drupal project.<\/p>\n\n<p>So, in no particular order:<\/p>\n\n<ul>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/admin\">Admin<\/a>:<\/strong> <br>The admin module provides UI improvements to the standard Drupal admin interface. I've just upgraded to the new <a href=\"http:\/\/drupal.org\/node\/835870\">6.x-2.0-beta4<\/a> version, and installed the newly-required <a href=\"http:\/\/code.developmentseed.org\/rubik\">Rubik<\/a>\/<a href=\"http:\/\/code.developmentseed.org\/tao\">Tao<\/a> themes. So far, so good!<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/better_perms\">Better Permissions<\/a>\/<a href=\"http:\/\/drupal.org\/project\/filter_perms\">Filter Permissions<\/a>: <br><\/strong>Basic permissions is a basic module which enhances the Drupal Permissions page to support collapsing and expanding permission rows. Filter permissions provides filters at the top of the Permissions page for easier management when your site has a large amount of roles and\/or permissions.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/better_formats\">Better Formats<\/a>: <br><\/strong>Better formats is a module to add more flexibility to Drupal's core input format system.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/node_clone\">Clone module<\/a>:<\/strong><br>Allows users to make a copy of an existing item of site content (a node) and then edit that copy.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/vertical_tabs\">Vertical Tabs<\/a>:<br><\/strong>Integrated into Drupal 7 core, this module adds vertical tabs to the node add and edit forms.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/context\">Context<\/a>:<br><\/strong>Context allows you to manage contextual conditions and reactions for different portions of your site. You can think of each context as representing a \"section\" of your site. For each context, you can choose the conditions that trigger this context to be active and choose different aspects of Drupal that should react to this active context.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/nodepicker\">Node Picker<\/a>:<\/strong><br>A rewrite of the module <a href=\"http:\/\/drupal.org\/project\/tinymce_node_picker\">TinyMCE Node Picker<\/a>. Allows you to easily create links to internal nodes.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/module_filter\">Module Filter<\/a>:<\/strong><br>What this module aims to accomplish is the ability to quickly find the module you are looking for without having to rely on the browsers search feature which more times than not shows you the module name in the 'Required by' or 'Depends on' sections of the various modules or even some other location on the page like a menu item.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/zenophile\">Zenophile<\/a>:<\/strong><br>Quickly create Zen subthemes.<\/li>\n<li><strong><a href=\"http:\/\/drupal.org\/project\/addanother\">Add Another<\/a>:<\/strong><br>Add another displays a message after a user creates a node, and\/or displays an \"Add another\" tab on nodes allowing them to make another node of the same type. You can control what roles and node types see this feature.<\/li>\n<\/ul>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal"
|
||
,"drupal-modules"
|
||
]
|
||
}, {
|
||
"title": "Create a Block of Social Media Icons using CCK, Views and Nodequeue",
|
||
"path": "/articles/create-block-social-media-icons-using-cck-views-and-nodequeue",
|
||
"is_draft": "false",
|
||
"created": "1277251200",
|
||
"excerpt": "How to create a block of social media icons in Drupal.",
|
||
"body": "<p>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 - <a href=\"http:\/\/twitter.com\/opdavies\">Twitter<\/a>, <a href=\"http:\/\/facebook.com\/opdavies\">Facebook<\/a> etc. I tried the <a href=\"http:\/\/drupal.org\/project\/follow\">Follow<\/a> module, but it lacked the option to add extra networks such my <a href=\"http:\/\/drupal.org\/user\/381388\">Drupal.org<\/a> account, and my <a href=\"http:\/\/oliverdavies.co.uk\/rss.xml\">RSS feed<\/a>. I started to create my own version, and then found <a href=\"http:\/\/www.hankpalan.com\/blog\/drupal-themes\/add-your-social-connections-drupal-icons\">this Blog post<\/a> by Hank Palan.<\/p>\n\n<p>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.<\/p>\n\n<p>However, instead of using a custom template (node-custom.tpl.php) file, I used the Views module.<\/p>\n\n<p>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.<\/p>\n\n<p>I also used the <a href=\"http:\/\/drupal.org\/project\/nodequeue\">Nodequeue<\/a> 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.<\/p>\n\n<p>To complete the process, I used the <a href=\"http:\/\/drupal.org\/project\/css_injector\">CSS Injector<\/a> module to add some additional CSS styling to position and space out the icons.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal"
|
||
,"views"
|
||
,"nodequeue"
|
||
,"oliverdavies.co.uk"
|
||
]
|
||
}, {
|
||
"title": "Improve JPG Quality in Imagecache and ImageAPI",
|
||
"path": "/articles/improve-jpg-quality-imagecache-and-imageapi",
|
||
"is_draft": "false",
|
||
"created": "1275436800",
|
||
"excerpt": "How to fix the quality of uploaded images in Drupal.",
|
||
"body": "<p>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.<\/p>\n\n<p>To increase the quality of your images, change the setting in the two following places:<\/p>\n\n<ul>\n<li>admin\/settings\/imageapi\/config<\/li>\n<li>admin\/settings\/image-toolkit<\/li>\n<\/ul>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"imagecache"
|
||
]
|
||
}, {
|
||
"title": "Quickly Import Multiples Images Using the Imagefield_Import Module",
|
||
"path": "/articles/quickly-import-multiples-images-using-imagefieldimport-module",
|
||
"is_draft": "false",
|
||
"created": "1275091200",
|
||
"excerpt": "How to use the Imagefield Import module.",
|
||
"body": "<p><strong>Thanks to Bob at <a href=\"http:\/\/mustardseedmedia.com\">Mustardseed Media<\/a> for <a href=\"http:\/\/twitter.com\/mustardseedinc\/status\/14713096905\">tweeting<\/a> about this module. It's undoubtedly saved me hours of work today alone!<\/strong><\/p>\n\n<p>I've recently started a personal project converting a website to Drupal. It's currently a static HTML\/CSS site which also uses the <a href=\"http:\/\/coppermine-gallery.net\">Coppermine Photo Gallery<\/a>. As part of building the new website, I wanted to move all the photos from the existing site onto the new one. However, with 1260 photos in 17 albums, this could have been a lengthy process!<\/p>\n\n<p>I created a new Drupal-powered Gallery as described in <a href=\"http:\/\/lullabot.com\/articles\/photo-galleries-views-attach\">this screencast<\/a> by <a href=\"http:\/\/twitter.com\/eaton\">Jeff Eaton<\/a> - using the CCK and Imagefield modules, and re-created each of my existing Gallery nodes. Using the <a href=\"http:\/\/drupal.org\/project\/imagefield_import\">Imagefield_Import<\/a> module, I was then able to quickly import the photos into the new Galleries.<\/p>\n\n<p>I downloaded all the photos from the previous Gallery via FTP, and installed and configured the Imagefield_Import module.<\/p>\n\n<p>I created an 'Import' folder, selected the target field and mode. In this case, I want each image to be imported into its own Photo node. I moved the photos for the first album into the Import folder, and loaded the 'Import Images' screen <em>(admin\/content\/imagefield_import)<\/em>.<\/p>\n\n<p>After clicking 'Import', a node is created for each photo, the image is uploaded, and added to the selected Gallery.<\/p>\n\n<p>Just another 1248 photos to go...<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"imagefield-import"
|
||
,"drupal"
|
||
,"drupal-6"
|
||
,"photo-gallery"
|
||
,"cck"
|
||
,"imagefield"
|
||
]
|
||
}, {
|
||
"title": "Create a Slideshow of Multiple Images Using Fancy Slide",
|
||
"path": "/articles/create-slideshow-multiple-images-using-fancy-slide",
|
||
"is_draft": "false",
|
||
"created": "1274745600",
|
||
"excerpt": "How to create a slideshow of images using Drupal\u2019s Fancy Slide module.",
|
||
"body": "<p>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 <a href=\"http:\/\/drupal.org\/project\/fancy_slide\">Fancy Slide<\/a> module. It's a jQuery Slideshow module with features that include integration with the <a href=\"http:\/\/drupal.org\/project\/cck\">CCK<\/a>, <a href=\"http:\/\/drupal.org\/project\/imagecache\">ImageCache<\/a> and <a href=\"http:\/\/drupal.org\/project\/nodequeue\">Nodequeue<\/a> modules.<\/p>\n\n<p>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.<\/p>\n\n<p>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 <a href=\"http:\/\/drupal.org\/project\/fancy_slide\">Fancy Slide<\/a> module. It's a jQuery Slideshow module with features that include integration with the <a href=\"http:\/\/drupal.org\/project\/cck\">CCK<\/a>, <a href=\"http:\/\/drupal.org\/project\/imagecache\">ImageCache<\/a> and <a href=\"http:\/\/drupal.org\/project\/nodequeue\">Nodequeue<\/a> modules.\nOnce the Images were added, I went to the Fancy Slide settings page and created the slideshow.<\/p>\n\n<p>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.<\/p>\n\n<p>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.<\/p>\n\n<pre><code class=\"language-php\"><?php print theme('fancy_slide', 1); ?>\n<\/code><\/pre>\n\n<p>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.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal"
|
||
,"drupal-6"
|
||
,"fancy-slide"
|
||
,"slideshow"
|
||
]
|
||
}, {
|
||
"title": "Quickly Create Zen Subthemes Using Zenophile",
|
||
"path": "/articles/zenophile",
|
||
"is_draft": "false",
|
||
"created": "1273449600",
|
||
"excerpt": "How to use the Zenophile module to create a Zen subtheme.",
|
||
"body": "<p>If you use the <a href=\"http:\/\/drupal.org\/project\/zen\">Zen<\/a> theme, then you should also be using the <a href=\"http:\/\/drupal.org\/project\/zenophile\">Zenophile<\/a> module!<\/p>\n\n<p>The Zenophile module allows you to very quickly create Zen subthemes from within your web browser, as well as editing options such as the site directory where it should be placed, the layout type (fixed or fluid), page wrapper and sidebar widths, and the placement of the sidebars.<\/p>\n\n<p>For more information about the Zenophile module, check out <a href=\"http:\/\/blip.tv\/file\/2427703\">this video<\/a> by <a href=\"http:\/\/elliottrothman.com\">Elliott Rothman<\/a>.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"drupal-modules"
|
||
,"drupal-theming"
|
||
,"zen"
|
||
,"zenophile"
|
||
]
|
||
}, {
|
||
"title": "Conditional Email Addresses in a Webform",
|
||
"path": "/articles/conditional-email-addresses-webform",
|
||
"is_draft": "false",
|
||
"created": "1273104000",
|
||
"excerpt": "How to send webform emails to a different email address based on another field.",
|
||
"body": "<p>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:<\/p>\n\n<pre><code class=\"language-ini\">Email address|Visible name\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<p>Then, when I went to the finished form, the category selection was available.<\/p>\n",
|
||
"tags": ["drupal-planet"
|
||
,"drupal-6"
|
||
,"conditional-email"
|
||
,"webform"
|
||
]
|
||
}, {
|
||
"title": "Using ImageCache and ImageCrop for my Portfolio",
|
||
"path": "/articles/using-imagecache-and-imagecrop-my-portfolio",
|
||
"is_draft": "false",
|
||
"created": "1272412800",
|
||
"excerpt": "How to create thumbnail images using the ImageCache and ImageCrop modules.",
|
||
"body": "<p>Whilst working on my own portfolio\/testimonial website, I decided to have a portfolio page displaying the name of each site and a thumbnail image. For this Blog post, I'll be using a site called <a href=\"http:\/\/popcornstrips.com\">Popcorn Strips<\/a> which I built for a friend earlier this year as an example.<\/p>\n\n<p>I created a content type called 'Project' with a CCK ImageField called 'Screenshot'. I created a project called <a href=\"http:\/\/popcornstrips.com\">Popcorn Strips<\/a>, used the <a href=\"https:\/\/addons.mozilla.org\/addon\/1146\">ScreenGrab<\/a> add-on for Mozilla Firefox to take a screenshot of the website, and uploaded it to the project node.<\/p>\n\n<p>I created a View to display the published projects, and an ImageCache\npreset to create the thumbnail image by scaling and cropping the image\nto a size of 200x100 pixels.<\/p>\n\n<p>Although, this automatically focused the crop on the centre of the image, whereas I wanted to crop from the top and left of the image - showing the site's logo and header.<\/p>\n\n<p>I installed the <a href=\"http:\/\/drupal.org\/project\/imagecrop\">ImageCrop<\/a> module, which add a jQuery crop function to the standard ImageCache presents. I removed the original Scale and Crop action and replaced it with a Scale action with a width of 200px.<\/p>\n\n<p>I then added a new 'Javascript crop' action with the following settings:<\/p>\n\n<ul>\n<li>Width: 200px<\/li>\n<li>Height: 100px<\/li>\n<li>xoffset: Left<\/li>\n<li>yoffset: Top<\/li>\n<\/ul>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-6"
|
||
,"cck"
|
||
,"imagecache"
|
||
,"imagecrop"
|
||
,"imagefield"
|
||
]
|
||
}, {
|
||
"title": "Style Drupal 6's Taxonomy Lists with PHP, CSS and jQuery",
|
||
"path": "/articles/style-drupal-6s-taxonomy-lists-php-css-and-jquery",
|
||
"is_draft": "false",
|
||
"created": "1270425600",
|
||
"excerpt": "Getting started with Drupal theming by styling Drupal\u2019s taxonomy lists.",
|
||
"body": "<p>Whilst developing this, and other Drupal websites for clients, I decided that I wanted to categorise content using the taxonomy system. However, I wasn't happy with the way that Drupal displayed the terms lists by default, and I started comparing this to other websites that I look at.<\/p>\n\n<p>To start with, I wanted to have something that described what the list was displaying - like in the second example above. I wanted to have the words 'Posted in' displayed before the list of terms. To do this, I had to edit the node template file that exists within my theme folder (sites\/all\/themes). As I only wanted this change to affect my Blog posts, the file that I needed to change is <strong>node-blog.tpl.php<\/strong><\/p>\n\n<p>I scrolled down until I found the piece of code that displayed the terms list:<\/p>\n\n<pre><code class=\"language-php\"><?php if ($terms): ?>\n <div class=\"terms terms-inline\">\n <?php print t('Posted in') . $terms; ?>\n <\/div>\n<?php endif; ?>\n<\/code><\/pre>\n\n<p>Adding <code>print t(' Posted in ')<\/code> will print the words 'Posted in' before outputing the terms.<\/p>\n\n<p>I then added some CSS to re-size the spacing between the items, and then add the commas between them to seperate them:<\/p>\n\n<pre><code class=\"language-css\">.terms ul.links li {\n margin-right: 1px;\n padding: 0;\n}\n\n.terms ul.links li:after {\n content: \",\";\n}\n\n.terms ul.links li.last:after {\n content: \".\";\n}\n<\/code><\/pre>\n\n<p>I created a file named <strong>script.js<\/strong> in my theme folder with the following code in it. After clearing Drupal's caches, this file is automatically recognised by Drupal 6.<\/p>\n\n<pre><code class=\"language-js\">if (Drupal.jsEnabled) {\n $(document).ready(function() {\n $('.terms ul.links li.last').prev().addClass('test');\n })\n}\n<\/code><\/pre>\n\n<p>This code finds the last item in the list, uses <strong>.prev<\/strong> to select the one before it, and then uses <strong>.addClass<\/strong> to assign it the HTML class of \"test\". We can then use this class to target it with specific CSS.<\/p>\n\n<pre><code class=\"language-css\">.terms ul.links li.test:after {\n content: \" and\";\n}\n<\/code><\/pre>\n",
|
||
"tags": ["drupal-6"
|
||
,"drupal-planet"
|
||
,"drupal-theming"
|
||
,"taxonomy"
|
||
]
|
||
}, {
|
||
"title": "Rebuilding Acquia’s Dashboard with Vue.js and Tailwind CSS",
|
||
"path": "/articles/rebuilding-acquia-dashboard-with-vuejs-tailwind-css",
|
||
"is_draft": "true",
|
||
"created": "",
|
||
"excerpt": "How I rebuilt Acquia\u2019s dashboard using Vue.js and Tailwind CSS.",
|
||
"body": "<p>After <a href=\"\/blog\/rebuilding-bartik-with-vuejs-tailwind-css\">rebuilding Drupal\u2019s Bartik theme<\/a>, I\u2019ve now used <a href=\"https:\/\/vuejs.org\">Vue.js<\/a> and <a href=\"https:\/\/tailwindcss.com\">Tailwind CSS<\/a> to rebuild another Drupal related UI - this time it\u2019s <a href=\"https:\/\/www.acquia.com\">Acquia\u2019s<\/a> web hosting dashboard. Again, you can <a href=\"https:\/\/rebuilding-acquia.oliverdavies.uk\">view the site on Netlify<\/a> and <a href=\"https:\/\/github.com\/opdavies\/rebuilding-acquia\">the code on GitHub<\/a>.<\/p>\n\n<h2 id=\"why%3F\">Why?<\/h2>\n\n<p>The same as the Bartik rebuild, this was a good opportunity for me to gain more experience with new technologies - Vue in particular - and to provide another demonstration of how Tailwind CSS can be used.<\/p>\n\n<p>Like the Bartik clone, this was originally going to be another single page rebuild, however after completing the first page I decided to expand it to include three pages which also gave me the opportunity to use <a href=\"https:\/\/router.vuejs.org\">Vue Router<\/a> - something that I had not used previously - and to organise a multi-page Vue application.<\/p>\n\n<h2 id=\"configuring-vue-router\">Configuring Vue Router<\/h2>\n\n<p><code>src\/router\/index.js<\/code>:<\/p>\n\n<pre><code class=\"js\">import Vue from 'vue'\nimport Router from 'vue-router'\nimport Applications from '@\/views\/Applications'\nimport Environment from '@\/views\/Environment'\nimport Environments from '@\/views\/Environments'\n\nVue.use(Router)\n\nexport default new Router({\n routes: [\n {\n path: '\/',\n name: 'applications',\n component: Applications,\n },\n {\n path: '\/:id\/environments',\n name: 'environments',\n component: Environments,\n props: true,\n },\n {\n path: '\/:id\/environments\/:environmentName',\n name: 'environment',\n component: Environment,\n props: true,\n }\n ]\n})\n<\/code><\/pre>\n\n<h2 id=\"passing-in-data\">Passing in data<\/h2>\n\n<p><code>src\/data.json<\/code><\/p>\n\n<pre><code class=\"json\">{\n \"applications\": {\n \"1\": {\n \"id\": 1,\n \"name\": \"Rebuilding Acquia\",\n \"machineName\": \"rebuildingacquia\",\n \"type\": \"Drupal\",\n \"level\": \"Enterprise\",\n \"environments\": {\n \"dev\": {\n \"name\": \"Dev\",\n \"url\": \"dev.rebuilding-acquia.com\",\n \"label\": \"develop\"\n },\n \"stage\": {\n \"name\": \"Stage\",\n \"url\": \"stg.rebuilding-acquia.com\",\n \"label\": \"master\"\n },\n \"prod\": {\n \"name\": \"Prod\",\n \"url\": \"rebuilding-acquia.com\",\n \"label\": \"tags\/2018-12-21\"\n }\n },\n \"tasks\": [\n {\n \"text\": \"Commit: fdac923 Merge branch 'update-password-policy' refs\/heads\/master\",\n \"user\": \"system\",\n \"times\": {\n \"display\": \"Dec 19, 2018 3:48:29 PM UTC +0000\",\n \"started\": \"Dec 19, 2018 3:48:29 PM UTC +0000\",\n \"completed\": \"Dec 19, 2018 3:48:29 PM UTC +0000\"\n },\n \"loading\": false,\n \"success\": true\n }\n ]\n }\n }\n}\n<\/code><\/pre>\n\n<h2 id=\"the-environments-page\">The Environments page<\/h2>\n\n<p>This was the first page that I rebuilt - the Environments page for an application that shows the information of the different configured environments.<\/p>\n\n<p>Vue Router is configured to show the<\/p>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/rebuilding-acquia-vue-tailwind\/3-environments.png\" alt=\"A screenshot of the rebuilt Environments page.\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The rebuilt Environments page for an application.\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<h2 id=\"the-applications-page\">The applications page<\/h2>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/rebuilding-acquia-vue-tailwind\/1-applications-grid.png\" alt=\"The rebuild Applications page, with applications displayed in a grid.\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The rebuilt Applications page - grid view\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/rebuilding-acquia-vue-tailwind\/2-applications-list.png\" alt=\"The rebuild Applications page, with applications displayed as a list.\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The rebuilt Applications page - list view\n <\/figcaption>\n <\/figure>\n<\/p>\n\n<h2 id=\"an-environment-page\">An environment page<\/h2>\n\n<p><figure class=\"block\">\n <img src=\"\/images\/blog\/rebuilding-acquia-vue-tailwind\/4-environment.png\" alt=\"A screenshot of the rebuilt Environment page.\" class=\"p-1 border\">\n <figcaption class=\"mt-2 mb-0 italic text-sm text-center text-gray-800\">\n The rebuilt page for an environment within an application.\n <\/figcaption>\n <\/figure>\n<\/p>\n",
|
||
"tags": ["drupal"
|
||
,"tailwind-css"
|
||
,"tweet"
|
||
,"vuejs"
|
||
,"drafts"
|
||
]
|
||
}, {
|
||
"title": "Using Drupal 8.8 with Symfony Local Server",
|
||
"path": "/articles/using-drupal-with-symfony-local-server",
|
||
"is_draft": "true",
|
||
"created": "",
|
||
"excerpt": "",
|
||
"body": "<p>https:\/\/symfony.com\/doc\/current\/setup\/symfony_server.html<\/p>\n\n<p><img src=\"\/iimages\/blog\/drupal-symfony-server\/terminal.png\" alt=\"\" \/><\/p>\n\n<h2 id=\"installation\">Installation<\/h2>\n\n<h2 id=\"different-php-versions\">Different PHP Versions<\/h2>\n\n<p>One of the most interesting features of the Symfony server is that it <a href=\"https:\/\/symfony.com\/doc\/current\/setup\/symfony_server.html#different-php-settings-per-project\">supports multiple versions of PHP<\/a> if you have them installed (e.g. via Homebrew), and setting different versions on any directory.<\/p>\n\n<p>As I work on different projects with have different requirements, this is a must for me.<\/p>\n\n<p>This is done simply by adding a <code>.php-version<\/code> file to the root of the project that contains the PHP version to use - e.g. <code>7.3<\/code>.<\/p>\n\n<p><a href=\"https:\/\/symfony.com\/doc\/current\/setup\/symfony_server.html#overriding-php-config-options-per-project\">Further PHP customisations can be made per project<\/a> by adding a <code>php.ini<\/code> file.<\/p>\n\n<h2 id=\"secure-sites-locally\">Secure Sites Locally<\/h2>\n\n<p><code>symfony server:ca:install<\/code><\/p>\n\n<p><code>symfony serve --no-tls<\/code><\/p>\n\n<h2 id=\"custom-domain-names\">Custom Domain Names<\/h2>\n\n<p>https:\/\/symfony.com\/doc\/current\/setup\/symfony_server.html#local-domain-names<\/p>\n\n<p><code>symfony proxy:domain:attach dransible<\/code><\/p>\n\n<h2 id=\"adding-databases-with-docker\">Adding Databases with Docker<\/h2>\n\n<p>The Symfony server has an integration with Docker for providing extra services such as databases that we\u2019ll need for Drupal.<\/p>\n\n<p>This is my <code>docker-compose.yaml<\/code> file which defines a <code>database<\/code> service for MySQL:<\/p>\n\n<pre><code class=\"yaml\">version: '2.1'\nservices:\n database:\n image: mysql:5.7\n ports: [3306]\n environment:\n MYSQL_ROOT_PASSWORD: secret\n volumes:\n - mysql-data:\/var\/lib\/mysql\nvolumes:\n mysql-data:\n<\/code><\/pre>\n\n<p>Because port 3306 is exposed, the server recognises it as a database service and automatically creates environment variables prefixed with <code>DATABASE_<\/code>. These can be seen by running <code>symfony var:export<\/code>.<\/p>\n\n<pre><code class=\"dotenv\">DATABASE_DATABASE=main\nDATABASE_DRIVER=mysql\nDATABASE_HOST=127.0.0.1\nDATABASE_NAME=main\nDATABASE_PASSWORD=secret\nDATABASE_PORT=32776\nDATABASE_SERVER=mysql:\/\/127.0.0.1:32776\nDATABASE_URL=mysql:\/\/root:secret@127.0.0.1:32776\/main?sslmode=disable&charset=utf8mb4\nDATABASE_USER=root\nDATABASE_USERNAME=root\nSYMFONY_DOCKER_ENV=1\nSYMFONY_TUNNEL=\nSYMFONY_TUNNEL_ENV=\n<\/code><\/pre>\n\n<p>Now I can use these environment variables within my <code>settings.php<\/code> file to allow Drupal to connect to the database service.<\/p>\n\n<pre><code class=\"php\">\/\/ web\/sites\/default\/settings.php\n\n\/\/ ...\n\nif ($_SERVER['SYMFONY_DOCKER_ENV']) {\n $databases['default']['default'] = [\n 'driver' => $_SERVER['DATABASE_DRIVER'],\n 'host' => $_SERVER['DATABASE_HOST'],\n 'database' => $_SERVER['DATABASE_NAME'],\n 'username' => $_SERVER['DATABASE_USER'],\n 'password' => $_SERVER['DATABASE_PASSWORD'],\n 'port' => $_SERVER['DATABASE_PORT'],\n 'prefix' => '',\n 'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\mysql',\n 'collation' => 'utf8mb4_general_ci',\n ];\n}\n<\/code><\/pre>\n\n<h2 id=\"installing-drupal\">Installing Drupal<\/h2>\n\n<p><code>..\/vendor\/bin\/drush site-install<\/code>:<\/p>\n\n<blockquote>\n <p>Error: Class 'Drush\\Sql\\Sql' not found in Drush\\Sql\\SqlBase::getInstance()<\/p>\n<\/blockquote>\n\n<p><code>symfony php ..\/vendor\/bin\/drush st<\/code><\/p>\n",
|
||
"tags": ["drupal"
|
||
,"drupal-8"
|
||
,"symfony"
|
||
,"drafts"
|
||
]
|
||
} ]
|
||
} |