Como exibir o formulário de registo de utilizador no front-end do sítio web?

O processo envolve 2 passos:

  1. mostrar o front-end do formulário
  2. guardar os dados sobre a submissão

Existem 3 abordagens diferentes que me vêm à cabeça para mostrar o front-end:

  • utilizar o formulário de registo incorporado, estilos de edição, etc. para o tornar mais “frontend like”
  • utilizar uma página/post do WordPress, e exibir o formulário utilizando um atalho
  • utilizar um modelo dedicado não ligado a nenhuma página/post, mas chamado por uma url específica

Para esta resposta vou utilizar a última. As razões são:

  • utilizar um formulário de registo incorporado pode ser uma boa ideia, personalizações profundas podem ser muito difíceis utilizando um formulário incorporado, e se também se quiser personalizar os campos do formulário a dor aumenta
  • utilizar uma página WordPress em combinação com um atalho, não é tão fiável, e também penso que os shorcodes não devem ser utilizados para funcionalidade, apenas para formatação e tal

1: Construir a url

Todos nós sabemos que o formulário de registo padrão de um site WordPress é muitas vezes um alvo para os spammers. Utilizar uma url personalizada é uma ajuda para resolver este problema. Além disso, quero também usar uma url variável, ou seja, a url do formulário de registo não deve ser sempre a mesma, o que torna a vida dos spammers mais difícil.O truque é feito usando um nonce no 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' ) );}

Utilizar estas funções é fácil de mostrar nos modelos um link para o formulário de registo mesmo que seja dinâmico.

2: Reconhecer o url, primeiro stub de Custom_Reg\Custom_Reg class

Agora precisamos de reconhecer o url. Para o efeito começarei a escrever uma classe, que será terminada mais tarde na resposta:

<?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; } }}

A função olhar para a primeira parte da url depois de home_url(), e se corresponder com o nosso nonce devolver VERDADEIRO. esta função será utilizada para verificar o nosso pedido e executar a acção necessária para exibir o nosso formulário.

3: A classe de formulário personalizado_Reg\Form>/h2>

Agora vou escrever uma classe, que será responsável por gerar a marcação do formulário.Vou usá-la também para armazenar numa propriedade o caminho do ficheiro modelo que deve ser usado para exibir o formulário.

<?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; }}

A classe gerar a marcação do formulário fazendo looping de todos os campos adicionados chamando create método em cada um deles.Cada campo deve ser exemplo de Custom_Reg\FieldInterface. Um campo adicional oculto é adicionado para verificação de não-conformidade. O método de formulário é ‘POST’ por defeito, mas pode ser definido para ‘GET’ usando setVerb método.Uma vez criado, a marcação é guardada dentro do método $form propriedade do objecto que é ecoada por output() método, ligada a 'custom_registration_form' gancho: no modelo do formulário, basta chamar que o formulário sairá.

4: o modelo padrão

Como eu disse, o modelo do formulário pode ser facilmente anulado, no entanto, precisamos de um modelo básico como um fallback.Escreverei aqui um modelo muito aproximado, mais uma prova de conceito do que um modelo 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: A interface Custom_Reg\FieldInterface

Todos os campos devem ser um objecto que implemente a seguinte interface

<?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 = '');}

Penso que os comentários explicam o que as classes que implementam esta interface devem fazer.

6: Adicionando alguns campos

Agora precisamos de alguns campos. Podemos criar um ficheiro chamado ‘fields.php’ onde definimos as classes de 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' ); }}

Utilizei uma classe base para definir o implemento padrão da interface, contudo, pode-se adicionar campos muito personalizados implementando directamente a interface ou alargando a classe base e anulando alguns métodos.

Neste ponto temos tudo para mostrar o formulário, agora precisamos de algo para validar e guardar os campos.

7: A classe Custom_Reg\Saver class

<?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; }}

Essa classe, tem 2 métodos principais, um (validate) que faz o loop dos campos, valida-os e guarda bons dados num array, o segundo (save) guarda todos os dados na base de dados e envia a palavra-passe via email para o novo utilizador.

8: Utilização de classes definidas: terminando a classe Custom_Reg

Agora podemos trabalhar novamente em Custom_Reg class, adicionando os métodos que “colam” o objecto definido e os fazem 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; } }}

O construtor da classe aceita uma instância de Form e uma de Saver.

init() método (usando checkUrl()) olhar para a primeira parte da url depois de home_url(), e se corresponder com o nonce correcto, verifica se o formulário já foi submetido, se assim for utilizando o objecto Saver, valida e guarda os dados do utilizador, caso contrário basta imprimir o formulário.

init() método também dispara o gancho de acção 'custom_reg_form_init' passando a instância do formulário como argumento: este gancho deve ser usado para adicionar campos, para configurar o modelo personalizado e também para personalizar o método do formulário.

9: Juntando as coisas

Agora precisamos de escrever o ficheiro plugin principal, onde podemos

  • exigir todos os ficheiros
  • carregar o textdomain
  • iniciar todo o processo usando instanciar Custom_Reg classe e call init() método sobre ele usando um gancho razoavelmente precoce
  • use o ‘custom_reg_form_init’ para adicionar os campos para formar a classe

So:

<?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: Tarefas em falta

Agora tudo está bem feito. Temos apenas de personalizar o modelo, provavelmente adicionando um ficheiro de modelo personalizado no nosso tema.

Podemos adicionar estilos e scripts específicos apenas à página de registo personalizado desta forma

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

Utilizando esse método, podemos consultar alguns scripts js para lidar com a validação do lado do cliente, por exemplo, este. A marcação necessária para fazer esse script funcionar pode ser facilmente manipulada editando o Custom_Reg\BaseField class.

Se quisermos personalizar o e-mail de registo, podemos usar o método padrão e tendo os dados personalizados guardados em meta, podemos fazer uso deles no e-mail.

A última tarefa que provavelmente queremos implementar é evitar o pedido de formulário de registo padrão, tão fácil como:

add_action( 'login_form_register', function() { exit(); } );

Todos os ficheiros podem ser encontrados numa Listagem aqui.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *