Slide 1

Slide 1 text

Peter Wilson ○ @pwcc ○ peterwilson.cc Burn it down: A case study in CMS replatforming

Slide 2

Slide 2 text

brisbanetimes.com.au August 27, 2017, 8:07pm

Slide 3

Slide 3 text

brisbanetimes.com.au August 27, 2017, 8:07pm August 27, 2017, 8:08pm

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

“ It was just another day in the newsroom

Slide 7

Slide 7 text

“ It was just another day writing content

Slide 8

Slide 8 text

humanmade.com/fairfax-media

Slide 9

Slide 9 text

$must_use_plugins = [ 'ffx-options/plugin.php', // Filter get_option calls. /* ... Vendor plugins snipped ... */ 'ffx-helpers/plugin.php', // Available during bootstrap. 'ffx-feature-flags/plugin.php', // Available during bootstrap. 'ffx-api-content/plugin.php', 'ffx-api-media/plugin.php', 'ffx-roles-capabilities/plugin.php', 'ffx-article-editor/plugin.php', 'ffx-live-articles/plugin.php', 'ffx-notifications/plugin.php', 'ffx-shortcake/plugin.php', 'ffx-brightcove/plugin.php', 'ffx-profiles/plugin.php', 'ffx-unpublish/plugin.php', 'ffx-wire-feed/plugin.php', 'ffx-image-editor/plugin.php', 'ffx-taxonomies/plugin.php', 'ffx-dashboard/plugin.php', 'ffx-publishing/plugin.php', 'ffx-tooltips/plugin.php', ];

Slide 10

Slide 10 text

Reimagining the tech-stack

Slide 11

Slide 11 text

The stack

Slide 12

Slide 12 text

The stack

Slide 13

Slide 13 text

The stack

Slide 14

Slide 14 text

The stack

Slide 15

Slide 15 text

The stack

Slide 16

Slide 16 text

The stack

Slide 17

Slide 17 text

The stack Media API Content API Cloudinary

Slide 18

Slide 18 text

Extending WordPress for enterprise

Slide 19

Slide 19 text

Publishing and Workflow

Slide 20

Slide 20 text

Media Library

Slide 21

Slide 21 text

Taxonomies

Slide 22

Slide 22 text

Tagging articles at scale

Slide 23

Slide 23 text

SELECT count(*) FROM wp_term_taxonomy WHERE taxonomy='ffx_tag'

Slide 24

Slide 24 text

2 2303

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

Content API
 tags endpoint ! Tags manager

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

GET 50 tags Content API

Slide 30

Slide 30 text

GET 50 tags wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); Content API

Slide 31

Slide 31 text

Managing terms makes a lot of 
 DB queries

Slide 32

Slide 32 text

wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );

Slide 33

Slide 33 text

# Check term exists (slug) SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy as tt ON tt.term_id = t.term_id WHERE t.slug = 'roxie-hart' AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1

Slide 34

Slide 34 text

SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy as tt ON tt.term_id = t.term_id AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1

Slide 35

Slide 35 text

SELECT tt.term_id, tt.term_taxonomy_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy as tt ON tt.term_id = t.term_id AND tt.parent = '105' AND tt.taxonomy = 'ffx_tag' ORDER BY t.term_id ASC LIMIT 1 # Check term exists (name) WHERE t.name = 'Roxie Hart'

Slide 36

Slide 36 text

# Check if (name) exists with same parent. SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('ffx_tag') AND t.name IN ('Roxie Hart') AND tt.parent = '105' ORDER BY t.name ASC

Slide 37

Slide 37 text

# Check slug is unique SELECT term_id FROM wp_terms as t WHERE t.slug = 'roxie-hart' ORDER BY t.term_id ASC LIMIT 1

Slide 38

Slide 38 text

# Insert the tag INSERT INTO `wp_terms` (`name`,`slug`,`term_group`) VALUES ('Roxie Hart', 'roxie-hart', 0)

Slide 39

Slide 39 text

Production, Chicago

Slide 40

Slide 40 text

# Check if the relationship data exists. SELECT tt.term_taxonomy_id FROM wp_term_taxonomy AS tt INNER JOIN wp_terms AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = 'ffx_tag' AND t.term_id = 23170

Slide 41

Slide 41 text

# Actually insert the relationship data. INSERT INTO `wp_term_taxonomy`
 ( `term_id`, `taxonomy`, `description`, `parent`, `count` ) VALUES (23170, 'ffx_tag', '', 105, 0)

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

# Logic check for duplicates. SELECT t.term_id, tt.term_taxonomy_id FROM wp_terms t INNER JOIN wp_term_taxonomy tt ON ( tt.term_id = t.term_id ) WHERE t.slug = 'roxie-hart' AND tt.parent = 105 AND tt.taxonomy = 'ffx_tag' AND t.term_id < 23170 AND tt.term_taxonomy_id != 23170

Slide 44

Slide 44 text

And that’s not all

Slide 45

Slide 45 text

# Warm term meta cache SELECT term_id, meta_key, meta_value FROM wp_ter # Check for meta key inserting SELECT meta_id FROM wp_termmeta WHERE meta_key = # Insert the meta key INSERT INTO `wp_termmeta` (`term_id`, `meta_key`

Slide 46

Slide 46 text

# Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name` = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy option INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`)

Slide 47

Slide 47 text

Production, Chicago

Slide 48

Slide 48 text

// Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time() + ( 10 * $page * MINUTE_IN_SECONDS ), 'ffx_import_some_terms_action', $tags ); }

Slide 49

Slide 49 text

Complex sites need scheduled tasks & asynchronous processing. Meet Cavalcade: A #WordPress jobs processing solution 
 humanmade.com/cavalcade Human Made @humanmadeltd 5:31 AM - 30 May 2017

Slide 50

Slide 50 text

NASA

Slide 51

Slide 51 text

NASA

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

We were so focused on the database, we forgot about HTTP

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

wp-cron.php

Slide 56

Slide 56 text

wp-cron.php Tags API

Slide 57

Slide 57 text

wp-cron.php Tags API Saved as cron job

Slide 58

Slide 58 text

wp-cron.php Tags API Saved as cron job

Slide 59

Slide 59 text

That’s what we forgot HTTP requests time out Prairie Kittin, flic.kr/p/a4Ujpv

Slide 60

Slide 60 text

update_option( 'cron', [ /* ... */ ] );

Slide 61

Slide 61 text

update_option( 'cron', [ /* ... */ ] );

Slide 62

Slide 62 text

update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );

Slide 63

Slide 63 text

update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );

Slide 64

Slide 64 text

update_option( 'cron', [ /* ... */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] ); wp_insert_term( 'Roxie Hart', 'ffx_tags', [ 'slug' => 'roxie-hart', 'parent' => '105', ] ); add_term_meta( 23170, '_ffx_id', 'h4rt' ); add_term_meta( 23170, '_ffx_term_meta', [ /* */ ] );

Slide 65

Slide 65 text

We reverted the original commit.

Slide 66

Slide 66 text

Complex sites need scheduled tasks & asynchronous processing. Meet Cavalcade: A #WordPress jobs processing solution 
 humanmade.com/cavalcade Human Made @humanmadeltd 5:31 AM - 30 May 2017

Slide 67

Slide 67 text

/** * Register hooks for WordPress. */ add_filter( 'pre_update_option_cron', 'update_cron_array', 10, 2 add_filter( 'pre_option_cron', 'get_cron_array' );

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

// Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time() + ( 10 * $page * MINUTE_IN_SECONDS ), 'ffx_import_some_terms_action', $tags ); }

Slide 70

Slide 70 text

// Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_some_terms_action', $tags ); }

Slide 71

Slide 71 text

// Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Get all the tags we need to process on the current page. $tags = get_tags_from_api( $page ); // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_some_terms_action', $tags ); }

Slide 72

Slide 72 text

// Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Set the arguments needed to process the current page. $args = [ 'page' => $page, 'qty' => 50 ]; // Schedule a single event to create or update terms. wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_tags_cron', $args ); }

Slide 73

Slide 73 text

// Update the scheduling lock. update_option( TAGS_SCHEDULING_LOCK_OPTION, time() ); // Get the total number of pages. $total_pages = get_total_pages(); for ( $page = 1; $page <= $total_pages; $page++ ) { // Set the arguments needed to process the current page. $args = [ 'page' => $page, 'qty' => 50 ]; // Schedule a single event to create or update terms. wp_schedule_single_event(

Slide 74

Slide 74 text

wp_schedule_single_event( time(), // Schedule all pages NOW! 'ffx_import_tags_cron', $args ); } // Delete the scheduling lock. delete_option( TAGS_SCHEDULING_LOCK_OPTION ); update_option( TAGS_SCHEDULED_LOCK_OPTION, time() );

Slide 75

Slide 75 text

[11] Worker out: [11] Worker err: [11] Worker ret: 0 [12] Worker status: Array ( [command] => wp cavalcade run 12 [pid] => 57 [running] => [signaled] => [stopped] => [exitcode] => 0 [termsig] => 0 [stopsig] => 0 ) 2018-01-27T01:09:22.458320545Z [12] Worker shutting down... [12] Worker out: [12] Worker err: [12] Worker ret: 0 [14] Running wp cavalcade run 14 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [14] Started worker [15] Running wp cavalcade run 15 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [15] Started worker [16] Running wp cavalcade run 16 (ffx/taxonomies/term_importer/import_tags_cron a:1:{i:0;a:5:{s:8:"tag_ty [16] Started worker [ ] Out of workers [ ] Out of workers [ ] Out of workers [ ] Out of workers [14] Worker status: Array

Slide 76

Slide 76 text

GET 50 tags wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); Content API

Slide 77

Slide 77 text

wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); GET 50 tags Content API "

Slide 78

Slide 78 text

wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ );

Slide 79

Slide 79 text

wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API " GET 50 tags Content API wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ ); wp_insert_term( /* Roxie Hart */ ); wp_insert_term( /* Billy Flynn */ ); wp_insert_term( /* Velma Kelly */ ); wp_insert_term( /* x 50 */ );

Slide 80

Slide 80 text

# Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name` = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy cache (335KB at 23K tags) INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`)

Slide 81

Slide 81 text

# Delete term hierarchy option DELETE FROM `wp_options` WHERE `option_name` = 'ffx_tag_children' # Get term hierarchy option (for reasons) SELECT option_value FROM wp_options WHERE option_name = 'ffx_tag_children # Select all terms in the taxonomy to work out hierarchy SELECT t.term_id, tt.parent, tt.count, tt.taxonomy FROM wp_terms AS t I # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN (9 # Update the term hierarchy cache (335KB at 23K tags) INSERT INTO `wp_options` (`option_name`, `option_value`, `autoload`) # Update the term # hierarchy cache # (335KB at 23K tags)

Slide 82

Slide 82 text

function import_tags( $taxonomy, $tags ) { /* * Disable term cache additions to speed up * the import, see comments in CMS-1175. */ wp_suspend_cache_addition( true ); foreach ( $tags[ $taxonomy ] as $tag ) { wp_insert_term( /* $tag */ ); update_term_meta( /* tag meta 1 */ ); update_term_meta( /* tag meta 2 */ ); } }

Slide 83

Slide 83 text

function import_tags( $taxonomy, $tags ) { /* * Disable term cache additions to speed up * the import, see comments in CMS-1175. */ wp_suspend_cache_addition( true ); foreach ( $tags[ $taxonomy ] as $tag ) { wp_insert_term( /* $tag */ ); update_term_meta( /* tag meta 1 */ ); update_term_meta( /* tag meta 2 */ ); } }

Slide 84

Slide 84 text

Read Cache Read Cache Read Cache Read Cache Write cache Write cache Write cache Write cache

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

Improving the media library

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

Limited crops four by default

Slide 91

Slide 91 text

Global crops happens on upload, calculated

Slide 92

Slide 92 text

defined by user, not by system Unpredictable 
 file names deh-logo.jpg

Slide 93

Slide 93 text

Same database images and articles share a db table

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

No content

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

The stack

Slide 101

Slide 101 text

No content

Slide 102

Slide 102 text

POST http://cms-authoring-local/wp/wp-admin/async-upload.php - action: upload-attachment

Slide 103

Slide 103 text

add_action( 'admin_init', 'ajax_upload_attachment', 0 ); /** * Ajax handler for uploading attachments * * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) {

Slide 104

Slide 104 text

add_action( 'admin_init', 'ajax_upload_attachment', 0 ); /** * Ajax handler for uploading attachments * * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) {

Slide 105

Slide 105 text

* * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) { return; } // Duplicate core functionality. wp_die(); }

Slide 106

Slide 106 text

* * Uploads from the media library are handled
 * by `async-upload.php`. * We can't override the hook so intercept `admin_init`. */ function ajax_upload_attachment() { // Only intercept the upload-attachment action. if ( ! isset( $_POST['action'] ) || 'upload-attachment' !== $_POST['action'] ) { return; } // Duplicate core functionality. }

Slide 107

Slide 107 text

{ 'altText': '', 'description': '', 'caption': '', 'credit': '', 'keywords': '', 'sha1': '7e51b009bf0e9097a1fd6ba339a78b6181c 'source': '', 'source_system_name': 'wordpress', 'fileDataURI': 'data:image/jpeg;base64,/9j/4AAQSkZJ }

Slide 108

Slide 108 text

} // Duplicate core functionality. $schema = Image_Schema\get_image_schema( $image, true ); wp_remote_request( 'https://api-media/v0/images', [ 'method' => 'POST', 'headers' => [ 'content-type' => 'application/json', ], 'body' => $schema, 'timeout' => 10, ] );

Slide 109

Slide 109 text

wp_remote_request( 'https://api-media/v0/images', [ 'method' => 'POST', 'headers' => [ 'content-type' => 'application/json', ], 'body' => $schema, 'timeout' => 10, ] ); wp_delete_post( $image_id, true ); wp_die(); }

Slide 110

Slide 110 text

No content

Slide 111

Slide 111 text

{ "success": true, "data": [ { "id": 4424 /* ... */ }, { "id": 4333 /* ... */ }, { "id": 4332 /* ... */ }, { "id": 4330 /* ... */ }, { "id": 4327 /* ... */ }, { "id": 4323 /* ... */ }, { "id": 4321 /* ... */ }, { "id": 4317 /* ... */ }, { "id": 4315 /* ... */ }, { "id": 4312 /* ... */ } ] } /wp-admin/admin-ajax.php?action=query-attachments

Slide 112

Slide 112 text

{ "id": 4424, "filename": "deh-logo.jpg", "url": "https://pwcc.cc/wp-content/uploads/2018/02/deh-logo.jpg "alt": "", "description": "", "caption": "", "name": "deh-logo", "dateFormatted": "February 8, 2018", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { "thumbnail": {}, "medium": {}, "large": {},

Slide 113

Slide 113 text

{ "altText": "Plaster casts from Dear Evan Hansen.", "caption": "The props department creates a new plaster cast for Dear "dateCreated": "2018-02-07T23:07:57.191Z", "credit": "Internet", "description": "Plaster casts Evan wears in Dear Evan Hansen.", "id": "78b576d29756c0f06ca5a1c450f4cf84bb69e8de", "keywords": "", "source": "Internet" }

Slide 114

Slide 114 text

/** * Bootstrap the library replacement. */ function bootstrap() { add_action( 'wp_ajax_query-attachments', 'ajax_query_attachments', 0 ); add_action( 'wp_ajax_get-attachment', 'ajax_get_attachment', 0 ); }

Slide 115

Slide 115 text

static.ffx.io/bd0d64a85c59655b815776ae46c3d14be7a6098e

Slide 116

Slide 116 text

static.ffx.io/images/t_resize_wp_admin/t_quality_best,f_auto/…

Slide 117

Slide 117 text

…/images/$width_357,$height_201/t_quality_best,f_auto/…

Slide 118

Slide 118 text

…/$multiply_3,$zoom_0.35,$ratio_1.8,$width_357,$x_600,$y_180/…

Slide 119

Slide 119 text

add_image_size( 'square1x1', 200, 200, true ); add_image_size( 'landscape3x2', 300, 200, true ); add_image_size( 'landscape16x9', 357, 201, true ); add_image_size( 'portrait2x3', 200, 300, true );

Slide 120

Slide 120 text

A complete 
 waste of time

Slide 121

Slide 121 text

add_filter( 'intermediate_image_sizes_advanced', /* sizes generated */ '__return_empty_array' /* [] - none */ );

Slide 122

Slide 122 text

get_attachment( 'al4n4b3ck' );

Slide 123

Slide 123 text

get_attachment( 'al4n4b3ck' ); is_int( 'al4n4b3ck' );

Slide 124

Slide 124 text

get_attachment( 'al4n4b3ck' ); is_int( 'al4n4b3ck' ); false

Slide 125

Slide 125 text

No content

Slide 126

Slide 126 text

/** * Retrieve attachment meta field for attachment ID. * * This matches the signature of `wp_get_attachment_metadata()` * modified for use with the Media API. * * @param string $attachment_id Attachment ID. Default ''. * @param bool $unfiltered True: filters are not run. * Default false. * @return mixed Attachment meta field. * False on failure. */ function get_attachment_metadata( /* ... */ ) { }

Slide 127

Slide 127 text

> var_dump( wp_get_attachment_metadata() ); Array( [width] => 2400, [height] => 1559, [file] => '2018/02/you-will-be-found.jpg', [sizes] => Array( [square1x1] => Array(width, height, file), [landscape3x2] => Array(width, height, file), [landscape16x9] => Array(width, height, file), [portrait2x3] => Array(width, height, file), [etc] => Array(width, height, file), ) )

Slide 128

Slide 128 text

> var_dump( API_Media\get_attachment_metadata() ); Array( [width] => 2400, [height] => 1559, [file] => 'bd0d64a85c59655b815776ae46c3d14be7a6098e', [url] => 'http://static.ffx.io/bd0d64a85c59655b81…98e', [sizes] => Array( [square1x1] => Array(width, height, file, url), [landscape3x2] => Array(width, height, file, url), [landscape16x9] => Array(width, height, file, url), [portrait2x3] => Array(width, height, file, url), [etc] => Array(width, height, file, url), ) )

Slide 129

Slide 129 text

API_Media\get_attachment_metadata( 'bd0d64...98e' ); API_Media\get_attachment( 'bd0d64...98e' ); API_Media\get_attachment_url( 'bd0d64...98e' );

Slide 130

Slide 130 text

{ "id": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "filename": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "url": "https://static.ffx.io/bd0d64a85c59655b815776ae46c3d14be "alt": "The Dear Evan Hansen cast perform You Will Be Found", "description": "Dear Evan Hansen cast perform You Will Be Found, the em "caption": "Dear Evan Hansen cast perform You Will Be Found, the em "name": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "dateFormatted": "February 8, 2018 09:02am", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { "square1x1": {}, "landscape3x2": {}, "landscape16x9": {},

Slide 131

Slide 131 text

{ "id": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "filename": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "url": "https://static.ffx.io/bd0d64a85c59655b815776ae46c3d14be "alt": "The Dear Evan Hansen cast perform You Will Be Found", "description": "Dear Evan Hansen cast perform You Will Be Found, the em "caption": "Dear Evan Hansen cast perform You Will Be Found, the em "name": "bd0d64a85c59655b815776ae46c3d14be7a6098e", "dateFormatted": "February 8, 2018 09:02am", "mime": "image/jpeg", "type": "image", "subtype": "jpeg", "sizes": { “thumbnail": {}, "landscape3x2": {}, "landscape16x9": {}, Not numeric That’s new These names have changed

Slide 132

Slide 132 text

No content

Slide 133

Slide 133 text

No content

Slide 134

Slide 134 text

wp.media.view ○ Attachment ○ AttachmentCompat ○ AttachmentFilters ○ Attachments ○ AttachmentsBrowser ○ AudioDetails ○ Button ○ ButtonGroup ○ Cropper ○ DateFilter ○ EditImage ○ EditorUploader ○ Embed ○ EmbedImage ○ EmbedLink ○ EmbedUrl ○ FocusManager ○ Frame ○ Iframe ○ ImageDetails ○ Label ○ MediaDetails ○ MediaFrame ○ Menu ○ MenuItem ○ Modal ○ PriorityList ○ Router ○ RouterItem ○ Search ○ Selection ○ Settings ○ Sidebar ○ SiteIconCropper ○ SiteIconPreview ○ Spinner ○ Toolbar ○ UploaderInline ○ UploaderStatus ○ UploaderStatusError ○ UploaderWindow ○ VideoDetails

Slide 135

Slide 135 text

Inherited from WordPress ○ 42 views ○ 6 models ○ 19 collections

Slide 136

Slide 136 text

Inherited from WordPress ○ 42 views ○ 6 models ○ 19 collections # Underscore # Backbone $ jQuery

Slide 137

Slide 137 text

/* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments = require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );

Slide 138

Slide 138 text

/* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments = require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );

Slide 139

Slide 139 text

/* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments = require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );

Slide 140

Slide 140 text

/* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments = require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );

Slide 141

Slide 141 text

/* Models */ wp.media.model.Attachment = require( './models/attachment' ); wp.media.model.Attachments = require( './models/attachments' ); /* Views */ wp.media.view.Attachment.Library = require( './views/attachment/library' ); wp.media.view.Attachment.Details = require( './views/attachment/details' ); wp.media.view.Attachment.Selection = require( './views/attachment/selection wp.media.view.Settings.AttachmentDisplay = require( './views/settings/attac wp.media.view.MediaFrame.Post = require( './views/frame/post' ); /* Misc */ wp.media.query = require( './query' );

Slide 142

Slide 142 text

/** * wp.media.query * * We're overriding this because we need to use our custom * Attachment model and collection. */ module.exports = function ( props = {} ) { return new Attachments( null, { props: _.extend( _.defaults( props, { orderby: 'date', order: 'DESC' } ), { query: true } ) } ); };

Slide 143

Slide 143 text

/** * wp.media.query * * We're overriding this because we need to use our custom * Attachment model and collection. */ module.exports = function ( props = {} ) { return new Attachments( null, { props: _.extend( _.defaults( props, { orderby: 'date', order: 'DESC' } ), { query: true } ) } ); };

Slide 144

Slide 144 text

/** * wp.media.view.Attachment.Library * * We're overriding this to use our custom template for images. */ module.exports = Library.extend( { template: function () { const prefix = ( this.model.get( 'type' ) === 'image' ) ? 'ffx-' : ''; const template = wp.template( `${prefix}attachment` ); return template.apply( this, arguments ); } } );

Slide 145

Slide 145 text

/** * wp.media.view.Attachment.Library * * We're overriding this to use our custom template for images. */ module.exports = Library.extend( { template: function () { const prefix = ( this.model.get( 'type' ) === 'image' ) ? 'ffx-' : ''; const template = wp.template( `${prefix}attachment` ); return template.apply( this, arguments ); } } );

Slide 146

Slide 146 text

A heavy touch server side 
 A lighter touch client side

Slide 147

Slide 147 text

No content

Slide 148

Slide 148 text

No content

Slide 149

Slide 149 text

/** * CropPreview Model * * @class * @augments Backbone.Model */ module.exports = Backbone.Model.extend( { defaults: { id: '', url: '', attachment_id: '', autoCrop: true, fileName: '', aspect: '', originalWidth: 0, cropWidth: 0, // Attributes below should not saved.

Slide 150

Slide 150 text

No content

Slide 151

Slide 151 text

No content

Slide 152

Slide 152 text

[img id="f23d41a8ebf536a52eca254e1bb44d996cf21d25" altText="..." caption="..." credit="..." source="..." description="..." aspect="1.5" cropWidth="300" autoCrop="false" 
 offsetX="-374.3478" offsetY="-85.9782"
 zoom="0.3062"][/img]

Slide 153

Slide 153 text

Publishing & workflow

Slide 154

Slide 154 text

No content

Slide 155

Slide 155 text

No content

Slide 156

Slide 156 text

// We don't need this, we have // the Publish Box of the Future™! remove_meta_box( 'submitdiv', 'post', 'side' );

Slide 157

Slide 157 text

// The Publish Box of the Future™! add_meta_box( 'ffx-submitdiv', __( 'Save & Publish', 'ffx' ), __NAMESPACE__ . '\\the_loading_icon', [ 'post' ], 'side', 'high' );

Slide 158

Slide 158 text

AJAX everything avoid full page refreshes

Slide 159

Slide 159 text

register_rest_field( 'post', 'ffx_writeoff', [ 'get_callback' => __NAMESPACE__ . '\\get_writeoff', 'update_callback' => __NAMESPACE__ . '\\set_writeoff', 'schema' => [ 'description' => __( 'Article writeoff', 'ffx' ), 'type' => 'string', ], ] );

Slide 160

Slide 160 text

register_rest_field( 'post', 'ffx_writeoff', [ 'get_callback' => __NAMESPACE__ . '\\get_writeoff', 'update_callback' => __NAMESPACE__ . '\\set_writeoff', 'schema' => [ 'description' => __( 'Article writeoff', 'ffx' ), 'type' => 'string', ], ] );

Slide 161

Slide 161 text

No content

Slide 162

Slide 162 text

No content

Slide 163

Slide 163 text

No content

Slide 164

Slide 164 text

Sixty-one 
 custom properties

Slide 165

Slide 165 text

register_rest_field( 'post', 'ffx_private', [ 'get_callback' => __NAMESPACE__ . '\\get_privacy', 'update_callback' => __NAMESPACE__ . '\\set_privacy', 'schema' => [ 'description' => __( 'Fairfax Visibility', 'ffx' ), 'type' => 'boolean', ], ] );

Slide 166

Slide 166 text

Inherited from WordPress # Underscore # Backbone $ jQuery

Slide 167

Slide 167 text

Inherited from WordPress # Underscore # Backbone $ jQuery % WordPress REST API client library

Slide 168

Slide 168 text

The Post model ○ author ○ categories ○ comment_status ○ content ○ date ○ date_gmt ○ excerpt ○ featured_media ○ ffx-legal-status ○ ffx-post-state ○ ffx-sources ○ ffx-tags ○ ffx_advertisements ○ ffx_advertiser_logo ○ ffx_advertiser_name ○ ffx_article_tool ○ ffx_authors ○ ffx_bespoke_url ○ ffx_brief ○ ffx_collab_authors ○ ffx_collab_editor ○ ffx_collab_watchers ○ ffx_comments ○ ffx_comments_open ○ ffx_commercial_content_ty pe ○ ffx_correction ○ ffx_correction_text ○ ffx_format ○ ffx_identifier ○ ffx_in_numbers ○ ffx_in_numbers_title ○ ffx_index_headline ○ ffx_intro ○ ffx_label ○ ffx_last_updated ○ ffx_major_update ○ ffx_misc_update ○ ffx_off_time ○ ffx_primary_tag ○ ffx_private ○ ffx_seo_description ○ ffx_seo_news_keywords ○ ffx_seo_noindex ○ ffx_seo_title ○ ffx_slack_channel ○ ffx_sponsor ○ ffx_sports_score ○ ffx_syndication ○ ffx_talking_points ○ ffx_talking_points_live_title ○ ffx_talking_points_title ○ ffx_url ○ ffx_url_content ○ ffx_url_slug ○ ffx_why_it_matters ○ ffx_writeoff ○ format ○ id ○ meta ○ password ○ ping_status ○ slug ○ status ○ sticky ○ template ○ title

Slide 169

Slide 169 text

> wp.api.models.Post.prototype.args.ffx_private 
 { required: false, description: "Fairfax Visibility.", type: "boolean" }

Slide 170

Slide 170 text

No content

Slide 171

Slide 171 text

No content

Slide 172

Slide 172 text

Post meta of 
 the past

Slide 173

Slide 173 text

wp_insert_post ○ update post content % create new revision ○ update taxonomies ○ update meta data

Slide 174

Slide 174 text

Update via REST API ○ update post content & create new revision ○ update taxonomies ○ update meta data

Slide 175

Slide 175 text

add_filter( 'rest_pre_dispatch', 'move_revision_callback', 10, 3 ); /** Move the 'wp_save_post_revision' callback for REST requests. */ function move_revision_callback( $result, $unused, $request ) { /* SNIP: Check for post update request */ // Move default `wp_save_post_revision` callback. remove_action( 'post_updated', 'wp_save_post_revision', 10 ); add_action( 'rest_request_after_callbacks', 'wp_save_post_revision'); return $result; // Support other filters. }

Slide 176

Slide 176 text

add_filter( 'rest_pre_dispatch', 'move_revision_callback', 10, 3 ); /** Move the 'wp_save_post_revision' callback for REST requests. */ function move_revision_callback( $result, $unused, $request ) { /* SNIP: Check for post update request */ // Move default `wp_save_post_revision` callback. remove_action( 'post_updated', 'wp_save_post_revision', 10 ); add_action( 'rest_request_after_callbacks', 'wp_save_post_revision'); return $result; // Support other filters. }

Slide 177

Slide 177 text

add_filter( 'rest_request_after_callbacks', 'reset_revision_cb' ); /** Reset 'wp_save_post_revision' after REST requests. */ function reset_revision_cb( $response ) { // Move default `wp_save_post_revision` callback. add_action( 'post_updated', 'wp_save_post_revision', 10 ); return $response; // Support other filters. }

Slide 178

Slide 178 text

The stack

Slide 179

Slide 179 text

The stack

Slide 180

Slide 180 text

Why WordPress?

Slide 181

Slide 181 text

[img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "", "bodyPlaceholders": { "2c62…d2": { "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,

Slide 182

Slide 182 text

[img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "", "bodyPlaceholders": { "2c62…d2": { "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,

Slide 183

Slide 183 text

[img id="9550846e3098113e9fe16878fcbc26e23580e8ea" altTex "body": "", "bodyPlaceholders": { "2c62…d2": { "type": "image", "data": { "id": "9550846e3098113e9fe16878fcbc26e23 "caption": "Lin-Manuel Miranda in Hamilton th "credit": "Production, Hamilton the Musical" "aspect": 1.5,

Slide 184

Slide 184 text

Project retro

Slide 185

Slide 185 text

No content

Slide 186

Slide 186 text

Committers 31

Slide 187

Slide 187 text

Commits 11,737

Slide 188

Slide 188 text

“ My job is code review and fooling myself that today I really will pick up a ticket. Me

Slide 189

Slide 189 text

My guilty secret

Slide 190

Slide 190 text

My guilty secret I like code review

Slide 191

Slide 191 text

Code review 
 is productivity

Slide 192

Slide 192 text

February 11, 2018 theage.com.au

Slide 193

Slide 193 text

February 12, 2018 theage.com.au

Slide 194

Slide 194 text

Slides and white paper ○ pwcc.cc/go/loop2018 Thank you