El proceso implica 2 pasos:
- mostrar el formulario en el front-end
- guardar los datos al enviarlo
Hay 3 enfoques diferentes que se me ocurren para mostrar el front-end:
- utilizar el formulario de registro incorporado, editar estilos, etc para hacerlo más «frontend»
- utilizar una página/post de WordPress, y mostrar el formulario usando un shortcode
- utilizar una plantilla dedicada no conectada con ninguna página/post, sino llamada por una url específica
- utilizar el formulario de registro incorporado puede ser una buena idea, las personalizaciones profundas pueden ser muy difíciles utilizando el formulario incorporado, y si uno también quiere personalizar los campos del formulario el dolor aumenta
- utilizar una página de WordPress en combinación con un shortcode, no es tan fiable, y también creo que los shorcodes no deben ser utilizados para la funcionalidad, sólo para el formato y tal
Para esta respuesta usaré la última. Las razones son:
1: Construir la url
Todos sabemos que el formulario de registro por defecto de un sitio WordPress suele ser un objetivo para los spammers. Usar una url personalizada es una ayuda para resolver este problema. Además quiero también utilizar una url variable, es decir, que la url del formulario de registro no sea siempre la misma, esto hace la vida de los spammers más difícil.El truco se hace usando un nonce en la url:
/*** Generate dynamic registration url*/function custom_registration_url() { $nonce = urlencode( wp_create_nonce( 'registration_url' ) ); return home_url( $nonce );}/*** Generate dynamic registration link*/function custom_registration_link() { $format = '<a href="%s">%s</a>'; printf( $format, custom_registration_url(), __( 'Register', 'custom_reg_form' ) );}
Usando estas funciones es fácil mostrar en las plantillas un enlace al formulario de registro aunque sea dinámico.
2: Reconocer la url, primer stub de la clase Custom_Reg\Custom_Reg
Ahora necesitamos reconocer la url. Para el pourpose voy a empezar a escribir una clase, que se terminará más adelante en la respuesta:
<?php// don't save, just a stubnamespace Custom_Reg;class Custom_Reg { function checkUrl() { $url_part = $this->getUrl(); $nonce = urlencode( wp_create_nonce( 'registration_url' ) ); if ( ( $url_part === $nonce ) ) { // do nothing if registration is not allowed or user logged if ( is_user_logged_in() || ! get_option('users_can_register') ) { wp_safe_redirect( home_url() ); exit(); } return TRUE; } } protected function getUrl() { $home_path = trim( parse_url( home_url(), PHP_URL_PATH ), '/' ); $relative = trim(str_replace($home_path, '', esc_url(add_query_arg(array()))), '/'); $parts = explode( '/', $relative ); if ( ! empty( $parts ) && ! isset( $parts ) ) { return $parts; } }}
La función mira la primera parte de la url después de home_url()
, y si coincide con nuestro nonce devuelve TRUE. Esta función se utilizará para comprobar nuestra petición y realizar la acción necesaria para mostrar nuestro formulario.
3: La clase Custom_Reg\Form
Ahora voy a escribir una clase, que será la responsable de generar el marcado del formulario.La usaré también para almacenar en una propiedad la ruta del archivo de la plantilla que debe usarse para mostrar el formulario.
<?php // file: Form.phpnamespace Custom_Reg;class Form { protected $fields; protected $verb = 'POST'; protected $template; protected $form; public function __construct() { $this->fields = new \ArrayIterator(); } public function create() { do_action( 'custom_reg_form_create', $this ); $form = $this->open(); $it = $this->getFields(); $it->rewind(); while( $it->valid() ) { $field = $it->current(); if ( ! $field instanceof FieldInterface ) { throw new \DomainException( "Invalid field" ); } $form .= $field->create() . PHP_EOL; $it->next(); } do_action( 'custom_reg_form_after_fields', $this ); $form .= $this->close(); $this->form = $form; add_action( 'custom_registration_form', array( $this, 'output' ), 0 ); } public function output() { unset( $GLOBALS ); if ( ! empty( $this->form ) ) { echo $this->form; } } public function getTemplate() { return $this->template; } public function setTemplate( $template ) { if ( ! is_string( $template ) ) { throw new \InvalidArgumentException( "Invalid template" ); } $this->template = $template; } public function addField( FieldInterface $field ) { $hook = 'custom_reg_form_create'; if ( did_action( $hook ) && current_filter() !== $hook ) { throw new \BadMethodCallException( "Add fields before {$hook} is fired" ); } $this->getFields()->append( $field ); } public function getFields() { return $this->fields; } public function getVerb() { return $this->verb; } public function setVerb( $verb ) { if ( ! is_string( $verb) ) { throw new \InvalidArgumentException( "Invalid verb" ); } $verb = strtoupper($verb); if ( in_array($verb, array( 'GET', 'POST' ) ) ) $this->verb = $verb; } protected function open() { $out = sprintf( '<form method="%s">', $this->verb ) . PHP_EOL; $nonce = '<input type="hidden" name="_n" value="%s" />'; $out .= sprintf( $nonce, wp_create_nonce( 'custom_reg_form_nonce' ) ) . PHP_EOL; $identity = '<input type="hidden" name="custom_reg_form" value="%s" />'; $out .= sprintf( $identity, __CLASS__ ) . PHP_EOL; return $out; } protected function close() { $submit = __('Register', 'custom_reg_form'); $out = sprintf( '<input type="submit" value="%s" />', $submit ); $out .= '</form>'; return $out; }}
La clase genera el marcado del formulario haciendo un bucle con todos los campos añadidos llamando al método create
en cada uno de ellos.Cada campo debe ser una instancia de Custom_Reg\FieldInterface
.Se añade un campo oculto adicional para la verificación de nonce. El método del formulario es ‘POST’ por defecto, pero puede ser configurado como ‘GET’ usando el método setVerb
.Una vez creado el marcado se guarda dentro de la propiedad del objeto $form
del que se hace eco el método output()
, enganchado en el hook 'custom_registration_form'
: en la plantilla del formulario, simplemente llamando a do_action( 'custom_registration_form' )
saldrá el formulario.
4: La plantilla por defecto
Como he dicho la plantilla para el formulario puede ser fácilmente anulada, sin embargo necesitamos una plantilla básica como un fallback.Voy a escribir aquí una plantilla muy aproximada, más una prueba de concepto que una plantilla real.
<?php// file: default_form_template.phpget_header();global $custom_reg_form_done, $custom_reg_form_error;if ( isset( $custom_reg_form_done ) && $custom_reg_form_done ) { echo '<p class="success">'; _e( 'Thank you, your registration was submitted, check your email.', 'custom_reg_form' ); echo '</p>';} else { if ( $custom_reg_form_error ) { echo '<p class="error">' . $custom_reg_form_error . '</p>'; } do_action( 'custom_registration_form' );}get_footer();
5: La interfaz Custom_RegieldInterface
Cada campo debe ser un objeto que implemente la siguiente interfaz
<?php // file: FieldInterface.phpnamespace Custom_Reg;interface FieldInterface { /** * Return the field id, used to name the request value and for the 'name' param of * html input field */ public function getId(); /** * Return the filter constant that must be used with * filter_input so get the value from request */ public function getFilter(); /** * Return true if the used value passed as argument should be accepted, false if not */ public function isValid( $value = NULL ); /** * Return true if field is required, false if not */ public function isRequired(); /** * Return the field input markup. The 'name' param must be output * according to getId() */ public function create( $value = '');}
Creo que los comentarios explican lo que deben hacer las clases que implementan esta interfaz.
6: Añadir algunos campos
Ahora necesitamos algunos campos. Podemos crear un archivo llamado ‘fields.php’ donde definimos las clases de los campos:
<?php// file: fields.phpnamespace Custom_Reg;abstract class BaseField implements FieldInterface { protected function getType() { return isset( $this->type ) ? $this->type : 'text'; } protected function getClass() { $type = $this->getType(); if ( ! empty($type) ) return "{$type}-field"; } public function getFilter() { return FILTER_SANITIZE_STRING; } public function isRequired() { return isset( $this->required ) ? $this->required : FALSE; } public function isValid( $value = NULL ) { if ( $this->isRequired() ) { return $value != ''; } return TRUE; } public function create( $value = '' ) { $label = '<p><label>' . $this->getLabel() . '</label>'; $format = '<input type="%s" name="%s" value="%s" class="%s"%s /></p>'; $required = $this->isRequired() ? ' required' : ''; return $label . sprintf( $format, $this->getType(), $this->getId(), $value, $this->getClass(), $required ); } abstract function getLabel();}class FullName extends BaseField { protected $required = TRUE; public function getID() { return 'fullname'; } public function getLabel() { return __( 'Full Name', 'custom_reg_form' ); }}class Login extends BaseField { protected $required = TRUE; public function getID() { return 'login'; } public function getLabel() { return __( 'Username', 'custom_reg_form' ); }}class Email extends BaseField { protected $type = 'email'; public function getID() { return 'email'; } public function getLabel() { return __( 'Email', 'custom_reg_form' ); } public function isValid( $value = NULL ) { return ! empty( $value ) && filter_var( $value, FILTER_VALIDATE_EMAIL ); }}class Country extends BaseField { protected $required = FALSE; public function getID() { return 'country'; } public function getLabel() { return __( 'Country', 'custom_reg_form' ); }}
He utilizado una clase base para definir la implementación de la interfaz por defecto, sin embargo, se pueden añadir campos muy personalizados implementando directamente la interfaz o extendiendo la clase base y sobrescribiendo algunos métodos.
En este punto tenemos todo para mostrar el formulario, ahora necesitamos algo para validar y guardar los campos.
7: La clase Custom_Reg\Saver
<?php// file: Saver.phpnamespace Custom_Reg;class Saver { protected $fields; protected $user = array( 'user_login' => NULL, 'user_email' => NULL ); protected $meta = array(); protected $error; public function setFields( \ArrayIterator $fields ) { $this->fields = $fields; } /** * validate all the fields */ public function validate() { // if registration is not allowed return false if ( ! get_option('users_can_register') ) return FALSE; // if no fields are setted return FALSE if ( ! $this->getFields() instanceof \ArrayIterator ) return FALSE; // first check nonce $nonce = $this->getValue( '_n' ); if ( $nonce !== wp_create_nonce( 'custom_reg_form_nonce' ) ) return FALSE; // then check all fields $it = $this->getFields(); while( $it->valid() ) { $field = $it->current(); $key = $field->getID(); if ( ! $field instanceof FieldInterface ) { throw new \DomainException( "Invalid field" ); } $value = $this->getValue( $key, $field->getFilter() ); if ( $field->isRequired() && empty($value) ) { $this->error = sprintf( __('%s is required', 'custom_reg_form' ), $key ); return FALSE; } if ( ! $field->isValid( $value ) ) { $this->error = sprintf( __('%s is not valid', 'custom_reg_form' ), $key ); return FALSE; } if ( in_array( "user_{$key}", array_keys($this->user) ) ) { $this->user = $value; } else { $this->meta = $value; } $it->next(); } return TRUE; } /** * Save the user using core register_new_user that handle username and email check * and also sending email to new user * in addition save all other custom data in user meta * * @see register_new_user() */ public function save() { // if registration is not allowed return false if ( ! get_option('users_can_register') ) return FALSE; // check mandatory fields if ( ! isset($this->user) || ! isset($this->user) ) { return false; } $user = register_new_user( $this->user, $this->user ); if ( is_numeric($user) ) { if ( ! update_user_meta( $user, 'custom_data', $this->meta ) ) { wp_delete_user($user); return FALSE; } return TRUE; } elseif ( is_wp_error( $user ) ) { $this->error = $user->get_error_message(); } return FALSE; } public function getValue( $var, $filter = FILTER_SANITIZE_STRING ) { if ( ! is_string($var) ) { throw new \InvalidArgumentException( "Invalid value" ); } $method = strtoupper( filter_input( INPUT_SERVER, 'REQUEST_METHOD' ) ); $type = $method === 'GET' ? INPUT_GET : INPUT_POST; $val = filter_input( $type, $var, $filter ); return $val; } public function getFields() { return $this->fields; } public function getErrorMessage() { return $this->error; }}
Esa clase, tiene 2 métodos principales, uno (validate
) que hace un bucle con los campos, los valida y guarda los datos buenos en un array, el segundo (save
) guarda todos los datos en la base de datos y envía la contraseña por email al nuevo usuario.
8: Uso de clases definidas: terminando la clase Custom_Reg
Ahora podemos trabajar de nuevo en la clase Custom_Reg
, añadiendo los métodos que «pegan» el objeto definido y los hacen funcionar
<?php // file Custom_Reg.phpnamespace Custom_Reg;class Custom_Reg { protected $form; protected $saver; function __construct( Form $form, Saver $saver ) { $this->form = $form; $this->saver = $saver; } /** * Check if the url to recognize is the one for the registration form page */ function checkUrl() { $url_part = $this->getUrl(); $nonce = urlencode( wp_create_nonce( 'registration_url' ) ); if ( ( $url_part === $nonce ) ) { // do nothing if registration is not allowed or user logged if ( is_user_logged_in() || ! get_option('users_can_register') ) { wp_safe_redirect( home_url() ); exit(); } return TRUE; } } /** * Init the form, if submitted validate and save, if not just display it */ function init() { if ( $this->checkUrl() !== TRUE ) return; do_action( 'custom_reg_form_init', $this->form ); if ( $this->isSubmitted() ) { $this->save(); } // don't need to create form if already saved if ( ! isset( $custom_reg_form_done ) || ! $custom_reg_form_done ) { $this->form->create(); } load_template( $this->getTemplate() ); exit(); } protected function save() { global $custom_reg_form_error; $this->saver->setFields( $this->form->getFields() ); if ( $this->saver->validate() === TRUE ) { // validate? if ( $this->saver->save() ) { // saved? global $custom_reg_form_done; $custom_reg_form_done = TRUE; } else { // saving error $err = $this->saver->getErrorMessage(); $custom_reg_form_error = $err ? : __( 'Error on save.', 'custom_reg_form' ); } } else { // validation error $custom_reg_form_error = $this->saver->getErrorMessage(); } } protected function isSubmitted() { $type = $this->form->getVerb() === 'GET' ? INPUT_GET : INPUT_POST; $sub = filter_input( $type, 'custom_reg_form', FILTER_SANITIZE_STRING ); return ( ! empty( $sub ) && $sub === get_class( $this->form ) ); } protected function getTemplate() { $base = $this->form->getTemplate() ? : FALSE; $template = FALSE; $default = dirname( __FILE__ ) . '/default_form_template.php'; if ( ! empty( $base ) ) { $template = locate_template( $base ); } return $template ? : $default; } protected function getUrl() { $home_path = trim( parse_url( home_url(), PHP_URL_PATH ), '/' ); $relative = trim( str_replace( $home_path, '', add_query_arg( array() ) ), '/' ); $parts = explode( '/', $relative ); if ( ! empty( $parts ) && ! isset( $parts ) ) { return $parts; } }}
El constructor de la clase acepta una instancia de Form
y una de Saver
.
init()
método (usando checkUrl()
) mira la primera parte de la url después de home_url()
, y si coincide con el nonce correcto, comprueba si el formulario ya fue enviado, si es así usando el objeto Saver
, valida y guarda los datos del usuario, de lo contrario sólo imprime el formulario.
El método init()
también dispara el action hook 'custom_reg_form_init'
pasando la instancia del formulario como argumento: este hook debe usarse para añadir campos, para configurar la plantilla personalizada y también para personalizar el método del formulario.
9: Poniendo las cosas en orden
Ahora tenemos que escribir el archivo principal del plugin, donde podamos
- requerir todos los archivos
- cargar el textdomain
- iniciar todo el proceso usando la clase
Custom_Reg
y llamar al métodoinit()
sobre él usando un hook razonablemente temprano - utilizar el
para añadir los campos a la clase del formulario
Entonces:
<?php /** * Plugin Name: Custom Registration Form * Description: Just a rough plugin example to answer a WPSE question * Plugin URI: https://wordpress.stackexchange.com/questions/10309/ * Author: G. M. * Author URI: https://wordpress.stackexchange.com/users/35541/g-m * */if ( is_admin() ) return; // this plugin is all about frontendload_plugin_textdomain( 'custom_reg_form', FALSE, plugin_dir_path( __FILE__ ) . 'langs'); require_once plugin_dir_path( __FILE__ ) . 'FieldInterface.php';require_once plugin_dir_path( __FILE__ ) . 'fields.php';require_once plugin_dir_path( __FILE__ ) . 'Form.php';require_once plugin_dir_path( __FILE__ ) . 'Saver.php';require_once plugin_dir_path( __FILE__ ) . 'CustomReg.php';/*** Generate dynamic registration url*/function custom_registration_url() { $nonce = urlencode( wp_create_nonce( 'registration_url' ) ); return home_url( $nonce );}/*** Generate dynamic registration link*/function custom_registration_link() { $format = '<a href="%s">%s</a>'; printf( $format, custom_registration_url(), __( 'Register', 'custom_reg_form' ) );}/*** Setup, show and save the form*/add_action( 'wp_loaded', function() { try { $form = new Custom_Reg\Form; $saver = new Custom_Reg\Saver; $custom_reg = new Custom_Reg\Custom_Reg( $form, $saver ); $custom_reg->init(); } catch ( Exception $e ) { if ( defined('WP_DEBUG') && WP_DEBUG ) { $msg = 'Exception on ' . __FUNCTION__; $msg .= ', Type: ' . get_class( $e ) . ', Message: '; $msg .= $e->getMessage() ? : 'Unknown error'; error_log( $msg ); } wp_safe_redirect( home_url() ); }}, 0 );/*** Add fields to form*/add_action( 'custom_reg_form_init', function( $form ) { $classes = array( 'Custom_Reg\FullName', 'Custom_Reg\Login', 'Custom_Reg\Email', 'Custom_Reg\Country' ); foreach ( $classes as $class ) { $form->addField( new $class ); }}, 1 );
10: Tareas pendientes
Ahora todo está bastante hecho. Sólo tenemos que personalizar la plantilla, probablemente añadiendo un archivo de plantilla personalizado en nuestro tema.
Podemos añadir estilos y scripts específicos sólo a la página de registro personalizada de esta manera
add_action( 'wp_enqueue_scripts', function() { // if not on custom registration form do nothing if ( did_action('custom_reg_form_init') ) { wp_enqueue_style( ... ); wp_enqueue_script( ... ); }});
Usando ese método podemos poner en cola algunos scripts js para manejar la validación del lado del cliente, por ejemplo este. El marcado necesario para que ese script funcione se puede manejar fácilmente editando la clase Custom_Reg\BaseField
.
Si queremos personalizar el email de registro, podemos usar el método estándar y al tener datos personalizados guardados en meta, podemos hacer uso de ellos en el email.
La última tarea que probablemente queramos implementar es evitar la petición al formulario de registro por defecto, tan fácil como:
add_action( 'login_form_register', function() { exit(); } );
Todos los archivos se pueden encontrar en un Gist aquí.