Traits &
Horizontal Design
michael stowe

January 17, 2014
MIKESTOWE .com
@mikegstowe

•  Open Source Contributor
•  Author, Speaker, and Consultant
•  10+ years experience hacking ...
WHAT WE’RE GONNA TALK ABOUT
•  Traits – What are they?
•  What is Horizontal Design?
•  Theory
•  Sample Vertical Applicat...
WHAT ARE TRAITS
“Traits are a mechanism for code reuse in
single inheritance languages such as PHP. A
Trait is intended to...
WHAT ARE TRAITS
“[They are] similar to a class, but intended to
group functionality in a fine-grained and
consistent way. ...
HUH?
To think of traits in another way, think of
traits as characteristics. People have unique
traits that makeup who they...
HUH?
In the same way Traits in PHP are collections
of methods that can be imported into classes.
Each stands alone by itse...
HUH?
Traits are designed to allow for code
reusability and extendibility. These groups of
methods are designed to be pulle...
QUICK LOOK
The Trait
<?php
trait MyTrait
{
public function test()
{
/* ... */
}
}!

The Class Utilizing the Trait
<?php
cl...
THE IDEA BEHIND HORIZONTAL DESIGN
The concept between horizontal design/
horizontal reuse is simple, reduce duplicate
code...
THE IDEA BEHIND HORIZONTAL DESIGN
Traditional Vertical Design
Interface

Parent Class

Child Class
THE IDEA BEHIND HORIZONTAL DESIGN
Traditional vertical design is very limiting. To help
address these limitations there ha...
THE IDEA BEHIND HORIZONTAL DESIGN
Vertical Design + Horizontal Design
Interface

Trait

Parent Class

Trait

Trait

Child ...
THE IDEA BEHIND HORIZONTAL DESIGN
Traits can reach other traits via the Class
<?php
trait traitOne
{
public function a()
{...
THE IDEA BEHIND HORIZONTAL DESIGN
Incredible Code Access and Reusability
Interface

Trait

Parent Class

Trait

Trait

Chi...
THE IDEA BEHIND HORIZONTAL DESIGN
Traits can be used in a vertical manner
<?php
trait traitOne
{
public function a()
{
ech...
THE IDEA BEHIND HORIZONTAL DESIGN
Incredible Code Access and Reusability
Interface

Trait

Parent Class

Trait

Trait

Chi...
THE IDEA BEHIND HORIZONTAL DESIGN
Traits seamlessly work together with encapsulation
<?php
trait traitOne
{
public functio...
BUILDING THE APP
In the next few slides we will take a look at
the difference between building a simple app
vertically, ve...
BUILDING A HORIZONTAL APP

<?php

Pull in the Traits, just like you would namespaces only within the class
/**
* @package ...
BUILDING A VERTICAL APP

<?php

Setup namespaces for class inclusion
use ressfpluginsextenders;
use ressfpluginsvalidators...
BUILDING A HORIZONTAL APP

<?php

Setup the __construct() method to do some basic work
/**
* Construct
* @return ressf
*/
...
BUILDING A VERTICAL APP

<?php

In the __construct() method setup properties to store the Extenders
and Validators objects...
BUILDING A VERTICAL APP

<?php

Do the same for the for user defined extenders and validators classes,
both which will nee...
BUILDING A VERTICAL APP

<?php

Also setup getters and setters for these properties:
/**
* Return the View
* @return strin...
BUILDING A HORIZONTAL APP

<?php

Because traits become part of the class, we can call the methods
natively

/**
* Handle ...
BUILDING A HORIZONTAL APP

<?php

Which can directly access the main class properties.
/**
* Set Cache Extender
* @param s...
BUILDING A VERTICAL APP

<?php

Now to call each method we can utilize the object referenced in the
other objects property...
BUILDING A VERTICAL APP

<?php

Which will in turn talk back to the ressf class like so:
/**
* Set Cache Extender
* @param...
THE DIFFERENCE
A quick breakdown of rewriting the ressf app to be PHP 5.3 compatible using
Dependency Injection (DI) inste...
THE DIFFERENCE
By using horizontal design we were not only able to make
the application more efficient, we were also able ...
THE BENEFITS
•  Reduces the amount of code needed
•  Reduces Dependency Injection requirements
•  Provides seamless integr...
THE BENEFITS
•  Allows for multiple interactions and eliminates vertical
asphyxiation/ dead ends
•  Plug and play friendly...
THE CHALLENGES
•  Property Conflicts – properties in traits and calling classes
must be exactly the same otherwise an erro...
THE DIFFERENCE BETWEEN CLASSES,
ABSTRACTS, INTERFACES
Traits are similar to abstract classes in that they cannot be
instan...
TRAIT HIERARCHY AND CLASS
OVERRIDES
Because traits are applied at a horizontal level there is no
hierarchy applied to the ...
CLASS OVERRIDES
Since the class contains the same method as the trait…
<?php
trait myTrait
{
public function test()
{
echo...
CLASS OVERRIDES

<?php

However, if a trait is included in a child class, and shares the
same method name with a method in...
TRAIT ALIASING
To use traits containing identically named methods or to make
a trait method available within a class we ca...
TRAIT ALIASING
<?php
trait traitOne
{
public function test()
{
echo 'one';
}
}
trait traitTwo
{
public function test()
{
e...
TRAIT ALIASING

<?php

When using multiple use declarations within your class it is
important to place the insteadof delca...
TRAIT ALIASING
Using multiple use statements:
class myClass
{
use traitOne, traitTwo
{
traitOne::test as one;
traitTwo::te...
TRAIT METHOD VISIBILITY

<?php

You can also modify the visibility state of a trait’s methods
through the use	
  declarati...
TRAIT METHOD DEPENDENCIES

<?php

Often times a trait method may require interaction with a
class method. To ensure the cl...
TRAIT PROPERTIES
One of the advantages of traits in PHP over other languages is
that PHP allows traits to contain properti...
TRAIT PROPERTIES

<?php

Because the visibility state of $test is different, the properties
are incompatible and PHP throw...
TRAIT PROPERTIES

<?php

Because the value of $test is different, the properties are
incompatible and PHP throws a fatal e...
TRAIT PROPERTIES

<?php

This example however will work because the property state,
type, and value is the same.
<?php
tra...
TRAIT FUNCTIONS
trait_exists()	
  – similar to class_exists(), this function
checks to see if the trait has been defined a...
REFLECTION CLASS
getTraits()	
  returns an array of all traits used in a class,
while getTraitNames()	
  returns an array ...
MORE USE CASES
MORE USE CASES
BASE CONTROLLER

MOD. CONTROLLER

API CONTROLLER

ACTUAL

What happens if we need to
modify the modified co...
MORE USE CASES
BASE CONTROLLER

MOD. CONTROLLER

ACTUAL

Traits allow us to pull in classes, properties,
and methods witho...
MORE USE CASES
BASE CONTROLLER

MOD. CONTROLLER

MODIFIERS

ACTUAL

API CONTROLLER

Traits also allow us to modify existin...
More Information:
http://php.net/manual/en/language.oop5.traits.php
GET THE ARTICLE

http://webandphp.com/issue-3
THANK YOU.
A big thank you to Constant Contact for
making this presentation possible
@mikegstowe
@ctct_api

visit mikestow...
Upcoming SlideShare
Loading in...5
×

Traits and Horizonal Design

4,139

Published on

One of the newest and most powerful features in PHP 5.4 is Traits, or the ability to implement horizontal design into your application. In these slides we will take a look at what traits are and how to use them (including examples of when to use horizontal design) as well as review the ReflectionClass and how it can be used to describe the traits in your code base.

Published in: Technology
0 Comments
9 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
4,139
On Slideshare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
47
Comments
0
Likes
9
Embeds 0
No embeds

No notes for slide

Traits and Horizonal Design

  1. 1. Traits & Horizontal Design michael stowe January 17, 2014
  2. 2. MIKESTOWE .com @mikegstowe •  Open Source Contributor •  Author, Speaker, and Consultant •  10+ years experience hacking PHP •  Zend Certified PHP 5.3 Software Engineer •  Developer Advocate with Constant Contact
  3. 3. WHAT WE’RE GONNA TALK ABOUT •  Traits – What are they? •  What is Horizontal Design? •  Theory •  Sample Vertical Application •  Horizontal Application •  Benefits of Horizontal Design •  Challenges with Horizontal Design •  Using Traits in PHP •  Trait vs. Classes, Abstracts, and Interfaces •  Trait Hierarchy and Class Overrides •  Trait Method Aliasing •  Adjusting Visibility •  Using Properties in Traits •  Trait Functions •  Traits and the Reflection Class
  4. 4. WHAT ARE TRAITS “Traits are a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.”
  5. 5. WHAT ARE TRAITS “[They are] similar to a class, but intended to group functionality in a fine-grained and consistent way. It is not possible to instantiate a Trait on it’s own. It is an addition to traditional inheritance and enables horizontal composition of behavior.”
  6. 6. HUH? To think of traits in another way, think of traits as characteristics. People have unique traits that makeup who they are- hair color, eye color, height, weight, likes, dislikes, etc. But it is the combination of these traits working together that make them who they are.
  7. 7. HUH? In the same way Traits in PHP are collections of methods that can be imported into classes. Each stands alone by itself (ie brown hair), but becomes part of something more when inherited into the class (he has brown hair and brown eyes).
  8. 8. HUH? Traits are designed to allow for code reusability and extendibility. These groups of methods are designed to be pulled in and used by any other class. Like Midas’ touch, they seamlessly become part of the class utilizing them.
  9. 9. QUICK LOOK The Trait <?php trait MyTrait { public function test() { /* ... */ } }! The Class Utilizing the Trait <?php class MyClass { use MyTrait; /* now contains test method */ }! <?php
  10. 10. THE IDEA BEHIND HORIZONTAL DESIGN The concept between horizontal design/ horizontal reuse is simple, reduce duplicate code while creating more extendable applications.
  11. 11. THE IDEA BEHIND HORIZONTAL DESIGN Traditional Vertical Design Interface Parent Class Child Class
  12. 12. THE IDEA BEHIND HORIZONTAL DESIGN Traditional vertical design is very limiting. To help address these limitations there has been an increased focus on Dependency Injection, or injecting classes into another class to allow the class to access additional methods and properties. However, even this approach is very limited in its capabilities. It’s important to note that Horizontal Design does not replace Dependency Injection, but rather ensures correct usage of it.
  13. 13. THE IDEA BEHIND HORIZONTAL DESIGN Vertical Design + Horizontal Design Interface Trait Parent Class Trait Trait Child Class Trait
  14. 14. THE IDEA BEHIND HORIZONTAL DESIGN Traits can reach other traits via the Class <?php trait traitOne { public function a() { echo 'Trait One Method'; } } trait traitTwo { public function b() { return $this->a(); } } class myClass { use traitOne; use traitTwo; } $a = new myClass(); $a->b(); // echos Trait One Method !
  15. 15. THE IDEA BEHIND HORIZONTAL DESIGN Incredible Code Access and Reusability Interface Trait Parent Class Trait Trait Child Class Trait Traits can access other trait methods/ properties of child and parent classes
  16. 16. THE IDEA BEHIND HORIZONTAL DESIGN Traits can be used in a vertical manner <?php trait traitOne { public function a() { echo 'Trait One Method'; } } trait traitTwo { public function a() { return parent::a(); } } class parentClass { use TraitOne; } class myClass extends parentClass { use traitTwo; } $a = new myClass(); $a->a(); // echos Trait One Method !
  17. 17. THE IDEA BEHIND HORIZONTAL DESIGN Incredible Code Access and Reusability Interface Trait Parent Class Trait Trait Child Class Trait Traits can natively utilize other trait methods/ properties via the classes
  18. 18. THE IDEA BEHIND HORIZONTAL DESIGN Traits seamlessly work together with encapsulation <?php trait traitOne { public function a() { echo 'Trait One Method'; } } <?php trait traitOne { public function a() { echo 'Trait One Method'; } } trait traitTwo { public function b() { return $this->a(); } } trait traitTwo { public function b() { return $this->a(); } } class parentClass { use TraitOne; } class parentClass { use TraitTwo; } class myClass extends parentClass { use traitTwo; } class myClass extends parentClass { use traitOne; } $a = new myClass(); $a->b(); // echos Trait One Method $a = new myClass(); $a->b(); // echos Trait One Method ! !
  19. 19. BUILDING THE APP In the next few slides we will take a look at the difference between building a simple app vertically, verses horizontally. The app will utilize the same 5 files in both cases.
  20. 20. BUILDING A HORIZONTAL APP <?php Pull in the Traits, just like you would namespaces only within the class /** * @package ressf * @category ressf */ class ressf { use ressfbasevalidators; use ressfbaseextenders; use ressfpluginsvalidators; use ressfpluginsextenders;! Full source on GitHub: http://github.com/mikestowe/ressf Branch: master
  21. 21. BUILDING A VERTICAL APP <?php Setup namespaces for class inclusion use ressfpluginsextenders; use ressfpluginsvalidators; /** * @package ressf * @category ressf */ class ressf {! Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  22. 22. BUILDING A HORIZONTAL APP <?php Setup the __construct() method to do some basic work /** * Construct * @return ressf */ public function __construct() { $this->tags = array_merge($this->baseTags, $this->tags); $this->extenders = array_merge($this->baseExtenders, $this->extenders); }! Full source on GitHub: http://github.com/mikestowe/ressf Branch: master
  23. 23. BUILDING A VERTICAL APP <?php In the __construct() method setup properties to store the Extenders and Validators objects. Since these objects will manipulate and make use of properties and methods within the instantiated ressf object we need to pass the ressf object to them as well. Now we can do the basic operations, utilizing the properties we just setup. /** * Construct * @return ressf */ public function __construct() { $this->extendersClass = new extenders($this); $this->validatorsClass = new validators($this); $this->tags = array_merge($this->validatorsClass->baseTags, $this->validatorsClass->tags); $this->extenders = array_merge($this->extendersClass->baseExtenders, $this->extendersClass->extenders); }! Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  24. 24. BUILDING A VERTICAL APP <?php Do the same for the for user defined extenders and validators classes, both which will need to extend the system defined base class: namespace ressfplugins; use ressfbaseextenders as baseExtenders; /** * User Defined Extenders Trait * @package ressf * @category ressf/plugins */ class extenders extends baseExtenders { protected $ressf; public function __construct($ressf) { $this->ressf = $ressf; }! Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  25. 25. BUILDING A VERTICAL APP <?php Also setup getters and setters for these properties: /** * Return the View * @return string */ public function getView() { return $this->view; } /** * Set the View * @param string * @return ressf */ public function setView($view) { $this->view = $view; return $this; } /** * Set Kill Process Switch * @param bool * @return ressf */ ! Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  26. 26. BUILDING A HORIZONTAL APP <?php Because traits become part of the class, we can call the methods natively /** * Handle Extenders * @param string * @return string */ private function handleExtenders($action) { if ($action == 'retrieve') { preg_match_all('/[ressf:([A-Za-z]+)=([^]]+)]/', $this->view, $matches); }! for ($i = 0; $i < count($matches[0]); $i++) { $this->{'set' . ucfirst($matches[1][$i])}($matches[2][$i]); $this->view = str_replace($matches[0][$i], '', $this->view); } Full source on GitHub: http://github.com/mikestowe/ressf Branch: master
  27. 27. BUILDING A HORIZONTAL APP <?php Which can directly access the main class properties. /** * Set Cache Extender * @param string * @return void */ public function setApcCache($cache = 'false') { $base = $this; $this->extenders['cache'] = array( 'doCache' => ($cache != 'false' && $cache != '0'), 'md5' => $base->detect() . '_' . md5($base->view), ); self::addAction('beforeRender', function() use ($base) { if ($base->extenders['cache']['doCache']) { $cachedView = apc_fetch($base->extenders['cache']['md5'], $isCached); if ($isCached) { $base->view = $cachedView; $base->killProcess = true; } } });! Full source on GitHub: http://github.com/mikestowe/ressf Branch: master
  28. 28. BUILDING A VERTICAL APP <?php Now to call each method we can utilize the object referenced in the other objects property, like so: /** * Handle Extenders * @param string * @return string */ private function handleExtenders($action) { if ($action == 'retrieve') { preg_match_all('/[ressf:([A-Za-z]+)=([^]]+)]/', $this->view, $matches); }! for ($i = 0; $i < count($matches[0]); $i++) { $this->getExtendersClass()->{'set' . ucfirst($matches[1][$i])}($matches[2][$i]); $this->view = str_replace($matches[0][$i], '', $this->view); } Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  29. 29. BUILDING A VERTICAL APP <?php Which will in turn talk back to the ressf class like so: /** * Set Cache Extender * @param string * @return void */ public function setApcCache($cache = 'false') { $base = $this->ressf; $base->setExtenders('cache', array( 'doCache' => ($cache != 'false' && $cache != '0'), 'md5' => $base->detect() . '_' . md5($base->getView()), )); ressf::addAction('beforeRender', function() use ($base) { $config = $base->getExtenders('cache'); if ($config['doCache']) { $cachedView = apc_fetch($config['md5'], $isCached); if ($isCached) { $base->setView($cachedView); $base->setKillProcess(true); } } });! Full source on GitHub: http://github.com/mikestowe/ressf Branch: php53
  30. 30. THE DIFFERENCE A quick breakdown of rewriting the ressf app to be PHP 5.3 compatible using Dependency Injection (DI) instead of utilizing Traits: 195 Additions | 39 Deletions https://github.com/mikestowe/ressf/compare/php53
  31. 31. THE DIFFERENCE By using horizontal design we were not only able to make the application more efficient, we were also able to reduce the amount of code by 34%! That’s just for a small, fairly simple application containing just 5 files and 454 lines of code! 195 Additions | 39 Deletions https://github.com/mikestowe/ressf/compare/php53
  32. 32. THE BENEFITS •  Reduces the amount of code needed •  Reduces Dependency Injection requirements •  Provides seamless integrations into classes •  Code can be reused by multiple classes •  Allows for properties of dependencies to be included
  33. 33. THE BENEFITS •  Allows for multiple interactions and eliminates vertical asphyxiation/ dead ends •  Plug and play friendly (traits play with other traits as long as there are no conflicts) •  No class name conflicts (as with namespaces)
  34. 34. THE CHALLENGES •  Property Conflicts – properties in traits and calling classes must be exactly the same otherwise an error is thrown •  Method Conflicts/ Overrides – traits containing the same methods may result in the wrong method being called •  Harder to navigate code (ie locating methods/ properties) •  Limited to PHP 5.4+
  35. 35. THE DIFFERENCE BETWEEN CLASSES, ABSTRACTS, INTERFACES Traits are similar to abstract classes in that they cannot be instantiated by themselves. However, unlike an abstract class traits are not extendable. You can use traits within other traits however, just as you would in a class through the use keyword. Traits can contain methods and properties, something that makes them unique in PHP.
  36. 36. TRAIT HIERARCHY AND CLASS OVERRIDES Because traits are applied at a horizontal level there is no hierarchy applied to the methods. Rather, if two traits on the same vertical plane contain identically named methods a fatal error will be thrown explaining which method could not be applied to the class. If a class has an identically named method it will override the trait method.
  37. 37. CLASS OVERRIDES Since the class contains the same method as the trait… <?php trait myTrait { public function test() { echo 'trait'; } } class myClass { use myTrait; public function test() { echo 'class'; } } (new myClass())->test(); ! The class method will be used, resulting in the script echoing out “class” <?php
  38. 38. CLASS OVERRIDES <?php However, if a trait is included in a child class, and shares the same method name with a method in the parent class, the trait will override the parent method, just as if the child class had the same method as the parent class. In this case parent methods will need to be referred to using the parent::method()  syntax. trait traitTwo { public function a() { return parent::a(); } }!
  39. 39. TRAIT ALIASING To use traits containing identically named methods or to make a trait method available within a class we can alias the trait using the as and insteadof keywords. The as keyword will give the method an alias to reference it by The insteadof keyword tells the compiler to use that traits method instead of a different identically named method from another trait.
  40. 40. TRAIT ALIASING <?php trait traitOne { public function test() { echo 'one'; } } trait traitTwo { public function test() { echo 'two'; } } class myClass { use traitOne, traitTwo { traitTwo::test insteadof traitOne; traitOne::test as one; } } (new myClass())->one(); (new myClass())->test(); ! <?php
  41. 41. TRAIT ALIASING <?php When using multiple use declarations within your class it is important to place the insteadof delcaration before the as declarations to avoid conflicts. <?php /* … */ class myClass { use traitOne, traitTwo { traitTwo::test insteadof traitOne; traitOne::test as one; } } !
  42. 42. TRAIT ALIASING Using multiple use statements: class myClass { use traitOne, traitTwo { traitOne::test as one; traitTwo::test insteadof traitOne; } use traitThree, traitFour { traitFour::testme insteadof traitThree; traitThree::testme as three; } } (new (new (new (new ! myClass())->one(); myClass())->test(); myClass())->three(); myClass())->testme(); <?php
  43. 43. TRAIT METHOD VISIBILITY <?php You can also modify the visibility state of a trait’s methods through the use  declaration: <?php trait traitOne { public function test() { echo 'one'; } } trait traitTwo { public function test() { echo 'two'; } } class myClass { use traitOne, traitTwo { traitOne::test as protected one; traitTwo::test insteadof traitOne; } }!
  44. 44. TRAIT METHOD DEPENDENCIES <?php Often times a trait method may require interaction with a class method. To ensure the class method is defined you can create an abstract method in the trait, which if called will throw a fatal error: <?php trait myTrait { public function test() { $this->doSomething(); } public abstract function doSomething(); } class myClass { use myTrait; } (new myClass())->test();!
  45. 45. TRAIT PROPERTIES One of the advantages of traits in PHP over other languages is that PHP allows traits to contain properties. This allows you to pull in a full set of code and it’s dependencies without having to manually add properties to your calling class. However, it is important to be careful when using properties as any properties declared in both the trait and the calling class must be identical in visibility, type, and value. The exception to this is when using a data-type of string for one, and a data-type int for another, as long as the state and value are identical.
  46. 46. TRAIT PROPERTIES <?php Because the visibility state of $test is different, the properties are incompatible and PHP throws a fatal error. <?php trait myTrait { public $test = 'hi'; } class myClass { use myTrait; protected $test = 'hi'; } new myClass(); !
  47. 47. TRAIT PROPERTIES <?php Because the value of $test is different, the properties are incompatible and PHP throws a fatal error. <?php trait myTrait { public $test = 'hi'; } class myClass { use myTrait; public $test = 'bye'; } new myClass(); !
  48. 48. TRAIT PROPERTIES <?php This example however will work because the property state, type, and value is the same. <?php trait myTrait { public $test = 'hi'; } class myClass { use myTrait; public $test = 'hi'; } new myClass(); !
  49. 49. TRAIT FUNCTIONS trait_exists()  – similar to class_exists(), this function checks to see if the trait has been defined and returns an boolean. get_declared_traits()  – similar to get_declared_classes(), this function returns an array of traits that have been declared.
  50. 50. REFLECTION CLASS getTraits()  returns an array of all traits used in a class, while getTraitNames()  returns an array of the trait names in a class. getTraitAliases()  returns an array of aliases linked to its original trait and method. isTrait()  returns whether or not a tested object is a trait http://php.net/manual/en/class.reflectionclass.php
  51. 51. MORE USE CASES
  52. 52. MORE USE CASES BASE CONTROLLER MOD. CONTROLLER API CONTROLLER ACTUAL What happens if we need to modify the modified controller? What if the API Controller needs new dependencies because our application was updated?
  53. 53. MORE USE CASES BASE CONTROLLER MOD. CONTROLLER ACTUAL Traits allow us to pull in classes, properties, and methods without having to create a dependency chain, leaving the modified controller easily accessible and updatable. API CONTROLLER
  54. 54. MORE USE CASES BASE CONTROLLER MOD. CONTROLLER MODIFIERS ACTUAL API CONTROLLER Traits also allow us to modify existing classes without overriding them in the vertical hierarchy. And keeps source code isolated and reusable.
  55. 55. More Information: http://php.net/manual/en/language.oop5.traits.php
  56. 56. GET THE ARTICLE http://webandphp.com/issue-3
  57. 57. THANK YOU. A big thank you to Constant Contact for making this presentation possible @mikegstowe @ctct_api visit mikestowe.com/slides for more on PHP and Web Development
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×