Upload
calderalearn
View
247
Download
0
Embed Size (px)
Citation preview
CalderaLabs.org
Join our SlackLoopConf Slack: #workshop-wpapi
We will be posting links, taking questions, and communicating throughout the day via Slack
CalderaLabs.org
Hi I'm JoshLead Developer: Caldera Labs
I make WordPress pluginsI teach about WordPressI wrote a book about the WordPress REST APII am a core contributor to WordPressI am a member of The WP Crowd
CalderaLearn.com
Hi, I'm RoySenior Software Engineer: The Walt Disney Company
I am a member of The WP CrowdI blog on roysivan.comI teach on Lynda.com & CalderaLearnPeople say Hi to me a lot
What We're Covering Today
REST API 101Building Custom REST APIsUnit Testing Custom REST APIs
LUNCH BREAKAngularJS (1.x) BasicsBuilding Decoupled Front-endsBuilding Plugin Admin Screens
Educational Philosophy
All code is or is based on real world projectsWe will show different ways of doing the same thing.
Please ask why it's different◇ we may give you a good answer
Stop us and ask questions
Structure For Today
foreach ( $sections as $section ) :
Concepts / LectureExample Code Walkthrough (you will be cloning locally)
Hands-onDIY GroupWalk Through Code Group
endforeach;
WARNING
This workshop is to help you understand the basics and some advanced technologies. Nothing will be production ready code.
What you need for today
IDE or text editor (PHPStorm, Sublime, etc.)Local WP install (VVV, DesktopServer, etc.)npm PHPUnit (optional, included in VVV)Composer (optional)AngularJS Batarang Chrome Extension
https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en
“
CalderaLearn.com
The API allows you to take WP data and put it in a bucket. What you do with that bucket is up to you.
-- Morten Rand Hendriksen@mor10
API Powered Stuff
Things you can build that are powered by the API
Phone appsCustom UI widgets on your site
Why use feed of another site, when you can APICustom Dashboards in the adminCustom WP Dashboard (YAS!)
User role based dashboard
add_action( 'init', 'my_book_cpt' ); function my_book_cpt() { $labels = array(...); $args = array( ... 'rest_controller_class' => 'WP_REST_Posts_Controller', 'show_in_rest' => true, 'rest_base' => 'books-api', ); register_post_type( 'book', $args );}
Add REST API Support To Post Type Registration
add_action( 'init', 'my_custom_post_type_rest_support', 25 ); function my_custom_post_type_rest_support() { global $wp_post_types; $post_type_name = 'book'; if( isset( $wp_post_types[ $post_type_name ] ) ) { $wp_post_types[$post_type_name]->show_in_rest = true; $wp_post_types[$post_type_name]->rest_base = $post_type_name; $wp_post_types[$post_type_name]->rest_controller_class = 'WP_REST_Posts_Controller'; } }
Add REST API Support To An Existing Post Type
function slug_get_meta_field( $object, $field_name, $request ) {
return get_post_meta( $object[ 'id' ], $field_name );
}
function slug_update_meta( $value, $object, $field_name ) {
if ( ! $value || ! is_string( $value ) ) {
return;
}
return update_post_meta( $object->ID, $field_name, strip_tags( $value ) );
}
Adding Custom Fields To A Response: Callbacks
add_action( 'rest_api_init', 'slug_register_spaceship' );
function slug_register_spaceship() {
register_api_field( 'post',
'starship',
array(
'get_callback' => 'slug_get_meta_field',
'update_callback' => 'slug_update_meta_field',
)
);
}
Adding Custom Fields To A Response: Registration
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'route_callback',
) );
} );
Registering A Route
class my_simple_route {
public function register_routes(){
$namespace = 'my-api/v1';
register_rest_route( $namespace, '/items', [
'methods' => 'GET',
'callback' => [ $this, 'get_items' ],
'permissions_callback' => [ $this, 'get_items_permissions' ]
] );
...
}
}
Registering Route(s)
class my_simple_route {
public function register_routes(){
...
register_rest_route( $namespace, '/items/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [ $this, 'get_item' ],
'permissions_callback' => [ $this, 'get_item_permissions' ]
] );
}
}
Registering Route(s)
register_rest_route( $this->namespace, '/items/(?P<id>\d+)', [
...
'args' => [
'type' => [
'required' => true,
'validate_callback' => [ $this, 'validate_type' ]
],
'number' => [
'default' => 5,
'sanitize_callback' => 'absint'
]
]
] );
Registering Route(s) : Defining Fields
Registering Route(s) : Field Sanitization Callback
Use to ensure data is safe.Defined using a callable.
Change data to a safe value.Return prepared value
Registering Route(s) : Field Validation Callback
Use to ensure data is correct.Defined using a callable.
Used to reject invalid requestsReturn true or false
Registering Route(s) : Field Validation Callback
public function validate_type( $value ){
if( !in_array( $value, [ 'big', 'small', 'very-small' ] ) ){
return false;
}
return true;
}
Registering Route(s) : Permissions Callback
Example: Make require login.public function get_items_permissions(){
if( is_user_logged_in() ){
return true;
}
return false;
}
Registering Route(s) : Permissions Callback
Example: Limit To Adminspublic function get_items_permissions(){
if( current_user_can( 'manage_options' ) ){
return true;
}
return false;
}
Registering Route(s) : Permissions Callback
Example: Allow Alwayspublic function get_items_permissions(){
return true;
}
Registering Route(s) : Callback
Do something with the requestGets an object of WP_REST_RequestShould return WP_REST_Response or WP_Error
WP_Rest_Response
Represents current requestContains:◇ Parameters◇ HeadersNo need to access $_GET, $_POST, $_REQUEST
Implements Arrayaccess$request->param( 'field_name' );$request[ 'field_name' ];
Registering Route(s) : Callback
public function get_item( WP_REST_Request $request ){
$id = $request[ 'id' ];
$item = slug_crud_get( $id );
if( ! empty( $item ) && ! is_wp_error( $item ) ){
return rest_ensure_response( $item );
}elseif ( is_wp_error( $item ) ){
return $item;
}else{
$response = new WP_REST_Response( 'No items found', 404 );
return $response;
}
}
Registering Route(s) : Initializing
add_action( 'rest_api_init', function(){
$route = new my_simple_route();
$route->register_routes();
});
Unit Testing 101
Make Sure Things Return What They Should Return
$this->assertEquals( 42, function_that_returns_42() );$this->assertSame( '42', function_that_returns_42() );$this->assertArrayHasKey( 'roy', array( 'roy' => 'hi' ) );
Google: "Pippin Williamson Unit Tests for WordPress Plugins"
Install PHPUnit
wget https://phar.phpunit.de/phpunit.pharchmod +x phpunit.pharmv phpunit.phar /usr/local/bin/phpunit
wget https://phar.phpunit.de/phpunit.pharphp phpunit.phar
Install WP CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.pharsudo mv wp-cli.phar /usr/local/bin/wp
An Example
public function test_get_items_author_query() {
$this->factory->post->create( array( 'post_author' => 4 ) );
$this->factory->post->create( array( 'post_author' => 4 ) );
$this->factory->post->create( array( 'post_author' => 2 );
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 3, count( $response->get_data() ) );
}
An Example
public function test_get_items_author_query() {
$this->factory->post->create( array( 'post_author' => 4 ) );
$this->factory->post->create( array( 'post_author' => 4 ) );
$this->factory->post->create( array( 'post_author' => 2 );
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param( 'author', 4 );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $response->get_data() ) );
}
Unit Test Case For REST API Endpoints
Create a reusable class that:
Create Instance of WP_REST_ServerBoot routesMake sure route is booted
Test Case Outline
class Test_API extends WP_UnitTestCase {
/** @var \WP_REST_Server*/
protected $server;
protected $namespaced_route = 'caldera-forms/v1';
public function setUp() {}
public function test_register_route() {}
public function test_endpoints() {}
}
Test Case: Set Up
protected $server;
public function setUp() {
parent::setUp();
/** @var \WP_REST_Server $wp_rest_server */
global $wp_rest_server;
$this->server = $wp_rest_server = new \WP_REST_Server;
do_action( 'rest_api_init' );
}
Test Case: Test Route Is Registered
public function test_register_route() {
$routes = $this->server->get_routes();
$this->assertArrayHasKey( $this->namespaced_route,
$routes );
}
Test Case: Test Endpoints Exist
public function test_endpoints() {
$the_route = $this->namespaced_route;
$routes = $this->server->get_routes();
foreach( $routes as $route => $route_config ) {
if( 0 === strpos( $the_route, $route ) ) {
$this->assertTrue( is_array( $route_config ) );
foreach( $route_config as $i => $endpoint ) {
$this->assertArrayHasKey( 'callback', $endpoint );
$this->assertArrayHasKey( 0, $endpoint[ 'callback' ], get_class( $this ) );
$this->assertArrayHasKey( 1, $endpoint[ 'callback' ], get_class( $this ) );
$this->assertTrue( is_callable( array( $endpoint[ 'callback' ][0], $endpoint[ 'callback' ][1] ) ) );
}
}
}
}
Test Your API Only!!!
Test internal logic elsewhereTest control of that logicTest response formatTrust core
Example Route Test
class Test_Hi extends Test_API {
protected $namespace = '/hi-api/v1/names';
public function test_list() {
$request = new WP_REST_Request( 'GET', $this->namespace );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertArrayHasKey( 'name', $data[0] );
$this->assertEquals( 'shawn', $data[0][ 'name' ] );
$this->assertArrayHasKey( 'name', $data[1] );
$this->assertEquals( 'roy', $data[1][ 'name' ] );
}
}
Example Route Test
class Test_Hi extends Test_API {
protected $namespace = '/hi-api/v1/names';
public function test_single(){
$request = new WP_REST_Request( 'GET', $this->namespace . '/roy' );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertArrayHasKey( 'name', $data );
$this->assertEquals( 'roy', $data[ 'name' ] );
}
}
“
CalderaLearn.com
AngularJS is the best JavaScript Framework.-- Roy Sivan
Senior WordPress Engineer at Disney (so you know he is legit)
Not WordPress
To get you through the basics of AngularJS we will be using sample data, not WP
Our First Project
No npm or gulp
We aren’t using any build tools, this is a pure sample
https://github.com/caldera-learn/angularjs-intro
Quicker & Easier
Quicker to get going to show overall concepts.
Honest Truth
We already have a few projects built in it that are ready to go
Why 1.x? 2 is in RC!
Roy is lazy
I am not lazy, but didn’t have time to learn it deeply enough yet to give a full workshop on it.
Josh Switched Teams
He used to be team NG1.
Now he is team VueJS.
HTML powered JavaScript
With Angular 1.x you can use HTML to do most things reserved for PHP.
Get the data using JS, template with HTML. No PHP needed.
Setting up the app
All functionality lives within 1 appWe use ng-app to encapsulate the app in the DOM, it can be used on any element including HTML.
Using it on the HTML encapsulates the whole DOM
Sample Data JSON
data.json
This file is going to be a sample of data, that we are going to use to build out a simple Angular App, we will then replace it with the WordPress REST API
Injectables
Injectables are objects which can be injected and used in
controllers, directives, etc.
We will be creating our own later...
AngularJS Controllers
Controller is all about $scopeA Controller is defined by a JavaScript constructor function that is used to augment the AngularJS Scope. When a Controller is attached to the DOM via the ng-controller directive, AngularJS will instantiate a new Controller object, using the specified Controller's constructor function
All new data to be used must be stored within $scope.your_key
ng-controller
All Together - JS
wpNG = {};
wpNG.app = ( function( app ){
console.log( 'initializing..' );
// define our app
app = angular.module('wpAngularApp', [])
.controller( 'listView', ['$scope', '$http', function( $scope, $http ) {
…
}]);
return app;
}(wpNG.app || {});
All Together - HTML
<div ng-app="wpAngularApp" id="app-container">
<div ng-controller="listView">
<!-- List View -->
</div>
</div>
First we must get data
$http is a jQuery AJAX wrapper & returns a promise
$http({method: ‘’, url: ‘’})
$http.get(url)
$http.post(url)
Because it returns a promise we use .then()
Say hello to JSON
JSON is the JavaScript Object Notation.
We display JSON with curly brackets
We work with JSON similar to PHP arrays
{{Object.key}} displays that key value
In PHP that would be $object[‘key’];
The loop HTML
<div ng-controller="listView">
<h2>Posts</h2>
<!-- Loop through $scope.posts -->
<article ng-repeat="post in posts">
<h2>{{post.title}}</h2>
</article>
</div>
Add more data
<article ng-repeat="post in posts">
<img ng-src="{{post.image}}" />
<h2>{{post.title}}</h2>
<div
class="post-content"
ng-bind-html="post.content | to_trusted">
</div>
</article>
Decoupled Front End
Decouple front end is a front end app that doesn’t live within
WordPress at all.
Your website: myawesomesite.com
Decoupled: myawesomeapp.com - but running on the same
data!
How cool is that!
Decoupled Use Cases
Phone App
Piece of functionality that communicates with WP data
Advanced, Unique UI
Combine with other APIs
Different Stack (decoupled app doesn’t need to be PHP)
Our decoupled App
npm
gulp
REST url that is publicly accessible (we will give you one)
Tacos, you always need tacos.
What you will need:
https://github.com/caldera-learn/decoupled-app
CORS
Allows browsers to make requests across domains.IS NOT SECURITY!!By default WordPress REST API is same-origin only.Set at rest_pre_serve_request hook
Allow All Domains For All Methods
add_action( 'rest_api_init', function() {
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
add_filter( 'rest_pre_serve_request', function( $value ) {
header( 'Access-Control-Allow-Origin: *' );
header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT,
DELETE' );
header( 'Access-Control-Allow-Credentials: true' );
return $value;
});
}, 15 );
Only Allow GET Requests
add_action( 'rest_api_init', function() {
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
add_filter( 'rest_pre_serve_request', function( $value ) {
$origin = get_http_origin();
if ( $origin ) {
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
}
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( site_url() ) );
header( 'Access-Control-Allow-Methods: GET' );
return $value;
});
}, 15 );
Allows From Certain Origins
add_action( 'rest_api_init', function() {remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );add_filter( 'rest_pre_serve_request', function( $value ) {
$origin = get_http_origin();if ( $origin && in_array( $origin, array(
'https://hiroy.club') ) ) {header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' );header( 'Access-Control-Allow-Credentials: true' );
}return $value;
});}, 15 );
AngularJS Factories
Factories allow you to create injectable objects. Inject $resource into the factory it to create an object that can automatically handle REST calls.
app.factory('Posts',function($resource){
return $resource( ngWP.config.api + ‘/:post_type/:ID?per_page=:per_page’, {
post_type: 'posts',
ID:'@id',
per_page: ngWP.config.posts_per_page,
});
})
AngularJS Factories cont’d
Posts.get()Posts.query() - like get, but expects arrayPosts.save()Posts.delete()Posts.remove()
Unlike $http these do not return promises
Local Storage
https://github.com/grevory/angular-local-storage
An AngularJS module that gives you access to the browser’s local storage with cookie fallback.
Local storage is a key/value store which we call on. In our example the regular posts (blog) will check for local storage first, before hitting the API.
UI-Router
https://github.com/angular-ui/ui-router
The Angular-UI project adds modules and functionality to Angular (think WP plugins for WP)
State Driven Routing& a lot of other functionality we won’t need.
Shows its true power when you have 1 view within another (post.detail within post.list)
Templates
Separating out templates into templates directory
Cleaner codeSeparation of views1 controller can have 1 template1 template can be powered by multiple controllersPure HTML (with JSON)
The config file
Create a file called config.js in /assets/js
var ngWP = ngWP || {};
ngWP.config = {
api: 'https://calderaforms.com/wp-json/',
posts_per_page: 5
// Not needed for our menu: 'app'
};
Stepping through the code
Main APP file
UI Router Definitions
Small controllers live here
Templates
Blog/Author List
Blog Detail
Product List
CalderaLabs.org
5.Plugin Admin Screens
With AngularJSThe UI Router
Making Mini-Apps To Make WordPress Better
Basic
Use add_admin_menu() to print basic HTMLUse ui-router to switch routes inside admin page.Use wp_localize_script() for config
Questions?
Use PHP or HTML files for templates?How to handle translations?
wp_localize_script()PHP templates
What’s Next?
Angular 2 Theme currently in developmentgithub.com/royboy789/Angular-Wordpress-Theme/tree/v7
Vue.JS - The new simple JS frameworkReactJS - What everyone else wants you to learn
Admin Theme Boilerplategithub.com/WordPress-Admin-JavaScript-Boilerplate/ReactJS-Boilerplate
Want more Roy & Josh teachings?
Caldera Learn teaches through 4-week live classroom style webinars. Teachers are Josh and/or Roy with future guest teachers