Burn it down: A case study in CMS replatforming

A presentation at LoopConf in February 2018 in Salt Lake City, UT, USA by Peter Wilson

Slide 1

Slide 1

Peter Wilson
○ @pwcc
○ peterwilson.cc

Burn it down: A case study in CMS replatforming

Slide 2

Slide 2

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

Slide 3

Slide 3

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

Slide 4

Slide 4

Slide 5

Slide 5

Slide 6

Slide 6

“ It was just another day in the newsroom

Slide 7

Slide 7

“ It was just another day writing content

Slide 8

Slide 8

humanmade.com/fairfax-media

Slide 9

Slide 9

*/ $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

Reimagining the tech-stack

Slide 11

Slide 11

The stack

Slide 12

Slide 12

The stack

Slide 13

Slide 13

The stack

Slide 14

Slide 14

The stack

Slide 15

Slide 15

The stack

Slide 16

Slide 16

The stack

Slide 17

Slide 17

The stack Media API Content API C loudinary

Slide 18

Slide 18

Extending WordPress for enterprise

Slide 19

Slide 19

Publishing and Workflow

Slide 20

Slide 20

Media Library

Slide 21

Slide 21

Taxonomies

Slide 22

Slide 22

Tagging articles

at scale

Slide 23

Slide 23

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

Slide 24

Slide 24

2 2 3 0 3

Slide 25

Slide 25

Slide 26

Slide 26

Slide 27

Slide 27

Content API 
 tags endpoint ! Tags manager

Slide 28

Slide 28

Slide 29

Slide 29

GET 50 tags Content API

Slide 30

Slide 30

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

Managing terms

makes a lot of 
 DB queries

Slide 32

Slide 32

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

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

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

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

C h e c k t e r m e x i s t s ( n a m e ) W H E R E t . n a m e

' R o x i e H a r t '

Slide 36

Slide 36

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

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

Insert the tag

INSERT INTO wp_terms (name,slug,term_group) VALUES ( 'Roxie Hart' , 'roxie-hart' , 0 )

Slide 39

Slide 39

Production, Chicago

Slide 40

Slide 40

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

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

Slide 43

Slide 43

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

And that’s not all

Slide 45

Slide 45

Warm term meta cache

SELECT term_id, meta_key, meta_value FROM wp_term

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

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' LIMIT 1

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
INNER

Warm the term meta cache for all (a bug)

SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN ( 95 ,

Update the term hierarchy option

INSERT INTO wp_options (option_name, option_value, autoload)

Slide 47

Slide 47

Production, Chicago

Slide 48

Slide 48

// 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

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

NASA

Slide 51

Slide 51

NASA

Slide 52

Slide 52

Slide 53

Slide 53

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

Slide 54

Slide 54

Slide 55

Slide 55

wp-cron.php

Slide 56

Slide 56

wp-cron.php Ta g s A P I

Slide 57

Slide 57

wp-cron.php Ta g s A P I Saved as cron job

Slide 58

Slide 58

wp-cron.php Ta g s A P I Saved as cron job

Slide 59

Slide 59

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

Slide 60

Slide 60

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

Slide 61

Slide 61

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

Slide 62

Slide 62

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

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

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

We reverted the original commit.

Slide 66

Slide 66

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

/**

  • 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

Slide 69

Slide 69

// 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

// 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

// 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

// 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

// 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(

time (), // Schedule all pages NOW!

Slide 74

Slide 74

// Schedule a single event to create or update terms.

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

[11] Worker shutting down... [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_type";s:4:"tags";s:10:"total_tags";i:23032;s:4:"size";i:50;s:5:"pages";i:461;s:4:"page";i:1;}}) [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_type";s:4:"tags";s:10:"total_tags";i:23032;s:4:"size";i:50;s:5:"pages";i:461;s:4:"page";i:2;}}) [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_type";s:4:"tags";s:10:"total_tags";i:23032;s:4:"size";i:50;s:5:"pages";i:461;s:4:"page";i:3;}}) [16] Started worker [ ] Out of workers [ ] Out of workers [ ] Out of workers [ ] Out of workers [14] Worker status: Array (

Slide 76

Slide 76

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

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

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

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

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' LIMIT 1

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
INNER

Warm the term meta cache for all (a bug)

SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN ( 95 ,

Update the term hierarchy cache (

335KB at 23K tags)

INSERT INTO wp_options (option_name, option_value, autoload)

Slide 81

Slide 81

Update the term

hierarchy

cache

(

335KB at 23K tags )

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 INNER # Warm the term meta cache for all (a bug) SELECT term_id, meta_key, meta_value FROM wp_termmeta WHERE term_id IN ( 95 # Update the term hierarchy cache ( 335KB at 23K tags) INSERT INTO wp_options (option_name, option_value, autoload)

Slide 82

Slide 82

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

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

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

Slide 85

Slide 85

Slide 86

Slide 86

Improving the media library

Slide 87

Slide 87

Slide 88

Slide 88

Slide 89

Slide 89

Slide 90

Slide 90

Limited crops four by default

Slide 91

Slide 91

Global crops happens on upload, calculated

Slide 92

Slide 92

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

Slide 93

Slide 93

Same database images and articles share a db table

Slide 94

Slide 94

Slide 95

Slide 95

Slide 96

Slide 96

Slide 97

Slide 97

Slide 98

Slide 98

Slide 99

Slide 99

Slide 100

Slide 100

The stack

Slide 101

Slide 101

Slide 102

Slide 102

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

Slide 103

Slide 103

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' ] ) {

return ;

Slide 104

Slide 104

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' ] ) {

return ;

Slide 105

Slide 105

  • 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

  • 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

{

'altText' :
'' ,

'description' :
'' ,

'caption' :
'' ,

'credit' :
'' ,

'keywords' :
'' ,

'sha1' :
'7e51b009bf0e9097a1fd6ba339a78b6181cbde0c' ,

'source' :
'' ,

'source_system_name' : 'wordpress' ,

'fileDataURI' :
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAUGBgcJBwoLCwoNDg0ODRMSEBASEx0V...' }

Slide 108

Slide 108

return ; }

// 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 ,

]

); wp_delete_post( $image_id , true );

Slide 109

Slide 109

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

Slide 111

Slide 111

{

"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

{

"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" : {},

"full" : {}

Slide 113

Slide 113

{

"altText" :
"Plaster casts from Dear Evan Hansen." ,

"caption" :
"The props department creates a new plaster cast for Dear Evan Hansen each night. It" ,

"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

/**

  • 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

static.ffx.io /bd0d64a85c59655b815776ae46c3d14be7a6098e

Slide 116

Slide 116

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

Slide 117

Slide 117

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

Slide 118

Slide 118

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

Slide 119

Slide 119

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

A complete 
 waste of time

Slide 121

Slide 121

add_filter(

'intermediate_image_sizes_advanced' ,

/* sizes generated */

'__return_empty_array'

/* [] - none */ );

Slide 122

Slide 122

get_attachment( 'al4n4b3ck' );

Slide 123

Slide 123

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

Slide 124

Slide 124

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

false

Slide 125

Slide 125

Slide 126

Slide 126

/**

  • 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

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

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

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

Slide 130

Slide 130

{

"id" :
"bd0d64a85c59655b815776ae46c3d14be7a6098e" ,

"filename" :
"bd0d64a85c59655b815776ae46c3d14be7a6098e" ,

"url" :
"https://static.ffx.io/bd0d64a85c59655b815776ae46c3d14be7a6098e.jpg" ,

"alt" :
"The Dear Evan Hansen cast perform You Will Be Found" ,

"description" :
"Dear Evan Hansen cast perform You Will Be Found, the emotional first act climax of Dear Evan Hansen" ,

"caption" :
"Dear Evan Hansen cast perform You Will Be Found, the emotional first act climax of Dear Evan Hansen" ,

"name" :
"bd0d64a85c59655b815776ae46c3d14be7a6098e" ,

"dateFormatted" : "February 8, 2018 09:02am" ,

"mime" :
"image/jpeg" ,

"type" :
"image" ,

"subtype" :
"jpeg" ,

"sizes" : {

"square1x1" : {},

"landscape3x2" : {},

"landscape16x9" : {},

"thumbnail" : {}

Slide 131

Slide 131

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

Slide 132

Slide 132

Slide 133

Slide 133

Slide 134

Slide 134

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

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

Slide 136

Slide 136

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

Underscore

Backbone $ jQuery

Slide 137

Slide 137

/* 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/attachment-display' ); wp . media . view . MediaFrame . Post

require ( './views/frame/post' ); /* Misc */ wp . media . query

require ( './query' );

Slide 138

Slide 138

/* 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/attachment-display' ); wp . media . view . MediaFrame . Post

require ( './views/frame/post' ); /* Misc */ wp . media . query

require ( './query' );

Slide 139

Slide 139

/* 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/attachment-display' ); wp . media . view . MediaFrame . Post

require ( './views/frame/post' ); /* Misc */ wp . media . query

require ( './query' );

Slide 140

Slide 140

/* 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/attachment-display' ); wp . media . view . MediaFrame . Post

require ( './views/frame/post' ); /* Misc */ wp . media . query

require ( './query' );

Slide 141

Slide 141

/* 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/attachment-display' ); wp . media . view . MediaFrame . Post

require ( './views/frame/post' ); /* Misc */ wp . media . query

require ( './query' );

Slide 142

Slide 142

/**

  • 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

/**

  • 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

/**

  • 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

/**

  • 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

A heavy touch server side 
 A lighter touch client side

Slide 147

Slide 147

Slide 148

Slide 148

Slide 149

Slide 149

/**

  • 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.

imageUrl : '' ,

Slide 150

Slide 150

Slide 151

Slide 151

Slide 152

Slide 152

[ 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

Publishing & workflow

Slide 154

Slide 154

Slide 155

Slide 155

Slide 156

Slide 156

// We don't need this, we have

// the Publish Box of the Future™! remove_meta_box(

'submitdiv' ,

'post' ,

'side' );

Slide 157

Slide 157

// T he Publish Box of the Future™! add_meta_box(

'ffx-submitdiv' , __( 'Save & Publish' , 'ffx' ),

NAMESPACE . ' \ the_loading_icon' , [ 'post' ],

'side' ,

'high' );

Slide 158

Slide 158

AJAX everything avoid full page refreshes

Slide 159

Slide 159

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

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

Slide 162

Slide 162

Slide 163

Slide 163

Slide 164

Slide 164

Sixty-one 
 custom properties

Slide 165

Slide 165

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

Inherited from WordPress

Underscore

Backbone $ jQuery

Slide 167

Slide 167

Inherited from WordPress

Underscore

Backbone $ jQuery % WordPress REST API client library

Slide 168

Slide 168

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

wp.api.models.Post.prototype.args.ffx_private


 { required:
false , description: "Fairfax Visibility." , type:
"boolean" }

Slide 170

Slide 170

Slide 171

Slide 171

Slide 172

Slide 172

Post meta of 
 the past

Slide 173

Slide 173

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

Slide 174

Slide 174

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

Slide 175

Slide 175

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

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

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

The stack

Slide 179

Slide 179

The stack

Slide 180

Slide 180

Why WordPress?

Slide 181

Slide 181

[ img id= "9550846e3098113e9fe16878fcbc26e23580e8ea" altText= "..." caption= "..." … ][/ img ] "body" : "<x-placeholder id='2c62…d2'></x-placeholder>" , "bodyPlaceholders" : {

"2c62…d2" : {

"type" : "image" ,

"data" : {

"id" :
"9550846e3098113e9fe16878fcbc26e23580e8ea" ,

"caption" :
"Lin-Manuel Miranda in Hamilton the Musical" ,

"credit" :
"Production, Hamilton the Musical" ,

"aspect" :
1.5 ,

"cropWidth" :
300 ,

Slide 182

Slide 182

[ img id= "9550846e3098113e9fe16878fcbc26e23580e8ea" altText= "..." caption= "..." … ][/ img ] "body" : "<x-placeholder id='2c62…d2'></x-placeholder>" , "bodyPlaceholders" : {

"2c62…d2" : {

"type" : "image" ,

"data" : {

"id" :
"9550846e3098113e9fe16878fcbc26e23580e8ea" ,

"caption" :
"Lin-Manuel Miranda in Hamilton the Musical" ,

"credit" :
"Production, Hamilton the Musical" ,

"aspect" :
1.5 ,

"cropWidth" :
300 ,

Slide 183

Slide 183

[ img id= "9550846e3098113e9fe16878fcbc26e23580e8ea" altText= "..." caption= "..." … ][/ img ] "body" : "<x-placeholder id='2c62…d2'></x-placeholder>" , "bodyPlaceholders" : {

"2c62…d2" : {

"type" : "image" ,

"data" : {

"id" :
"9550846e3098113e9fe16878fcbc26e23580e8ea" ,

"caption" :
"Lin-Manuel Miranda in Hamilton the Musical" ,

"credit" :
"Production, Hamilton the Musical" ,

"aspect" :
1.5 ,

"cropWidth" :
300 ,

Slide 184

Slide 184

Project retro

Slide 185

Slide 185

Slide 186

Slide 186

Committers 31

Slide 187

Slide 187

Commits 11,737

Slide 188

Slide 188

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

Slide 189

Slide 189

My guilty secret

Slide 190

Slide 190

My guilty secret I like code review

Slide 191

Slide 191

Code review


 is productivity

Slide 192

Slide 192

February 11, 2018 theage.com.au

Slide 193

Slide 193

February 12, 2018 theage.com.au

Slide 194

Slide 194

Slides and white paper

pwcc.cc/go/loop2018 Thank you