Peter Wilson
○ @pwcc
○ peterwilson.cc

Burn it down: A case study in CMS replatforming

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

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

“ It was just another day in the newsroom

“ It was just another day writing content

humanmade.com/fairfax-media

*/ $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' , ];

Reimagining the tech-stack

The stack

The stack

The stack

The stack

The stack

The stack

The stack Media API Content API C loudinary

Extending WordPress for enterprise

Publishing and Workflow

Media Library

Taxonomies

Tagging articles

at scale

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

2 2 3 0 3

Content API 
 tags endpoint ! Tags manager

GET 50 tags Content API

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

Managing terms

makes a lot of 
 DB queries

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' , [ /* */ ] );

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

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

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 '

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

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

Insert the tag

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

Production, Chicago

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

Actually insert the relationship data.

INSERT INTO wp_term_taxonomy 
 ( term_id, taxonomy,

`description`, `parent`, `count`

) VALUES ( 23170 , 'ffx_tag' , '' , 105 , 0 )

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

And that’s not all

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,

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)

Production, Chicago

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

); }

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

NASA

NASA

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

wp-cron.php

wp-cron.php Ta g s A P I

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

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

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

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

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

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' , [ /* */ ] );

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' , [ /* */ ] );

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' , [ /* */ ] );

We reverted the original commit.

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

/**

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

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

); }

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

); }

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

); }

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

); }

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

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

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

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

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 "

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 */ );

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 */ );

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)

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)

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 */ ); } }

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 */ ); } }

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

Improving the media library

Limited crops four by default

Global crops happens on upload, calculated

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

Same database images and articles share a db table

The stack

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

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 ;

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 ;

  • 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(); }

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

}

{

'altText' :
'' ,

'description' :
'' ,

'caption' :
'' ,

'credit' :
'' ,

'keywords' :
'' ,

'sha1' :
'7e51b009bf0e9097a1fd6ba339a78b6181cbde0c' ,

'source' :
'' ,

'source_system_name' : 'wordpress' ,

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

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 );

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(); }

{

"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

{

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

{

"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" }

/**

  • 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 ); }

static.ffx.io /bd0d64a85c59655b815776ae46c3d14be7a6098e

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

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

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

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 );

A complete 
 waste of time

add_filter(

'intermediate_image_sizes_advanced' ,

/* sizes generated */

'__return_empty_array'

/* [] - none */ );

get_attachment( 'al4n4b3ck' );

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

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

false

/**

  • 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( / ... */ ) { }

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

) )

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

) )

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

{

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

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

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

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

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

Underscore

Backbone $ jQuery

/* 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' );

/* 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' );

/* 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' );

/* 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' );

/* 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' );

/**

  • 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 } ) } ); };

/**

  • 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 } ) } ); };

/**

  • 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 ); } } );

/**

  • 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 ); } } );

A heavy touch server side 
 A lighter touch client side

/**

  • 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 : '' ,

[ 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 ]

Publishing & workflow

// We don't need this, we have

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

'submitdiv' ,

'post' ,

'side' );

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

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

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

'side' ,

'high' );

AJAX everything avoid full page refreshes

register_rest_field(

'post' ,

'ffx_writeoff' , [

'get_callback'
=> NAMESPACE . ' \ get_writeoff' ,

'update_callback' => NAMESPACE . ' \ set_writeoff' ,

'schema'
=> [

'description' => __( 'Article writeoff' , 'ffx' ),

'type'
=> 'string' , ], ] );

register_rest_field(

'post' ,

'ffx_writeoff' , [

'get_callback'
=> NAMESPACE . ' \ get_writeoff' ,

'update_callback' => NAMESPACE . ' \ set_writeoff' ,

'schema'
=> [

'description' => __( 'Article writeoff' , 'ffx' ),

'type'
=> 'string' , ], ] );

Sixty-one 
 custom properties

register_rest_field(

'post' ,

'ffx_private' , [

'get_callback'
=> NAMESPACE . ' \ get_privacy' ,

'update_callback' => NAMESPACE . ' \ set_privacy' ,

'schema'
=> [

'description' => __( 'Fairfax Visibility' , 'ffx' ),

'type'
=> 'boolean' , ], ] );

Inherited from WordPress

Underscore

Backbone $ jQuery

Inherited from WordPress

Underscore

Backbone $ jQuery % WordPress REST API client library

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

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


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

Post meta of 
 the past

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

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

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

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

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

The stack

The stack

Why WordPress?

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

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

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

Project retro

Committers 31

Commits 11,737

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

My guilty secret

My guilty secret I like code review

Code review


 is productivity

February 11, 2018 theage.com.au

February 12, 2018 theage.com.au

Slides and white paper
○

pwcc.cc/go/loop2018 Thank you