Créer un formulaire dans WordPress pour proposer un produit WooCommerce

Maintenant que notre base SQL WordPress et notre back-office sont prêts à accueillir des données, il nous faut un moyen de créer ces données ! Apprenons à créer un shortcode et développer un formulaire front-end WordPress pour proposer un produit WooCommerce non-publié.

Si vous avez manqué le début de cette série de tutoriels, rendez-vous dans la première partie d’introduction pour en savoir plus 😉 Sachez aussi que ce plugin est téléchargeable gratuitement et  que vous pouvez découvrir son code sur GitHub.

Ce formulaire public va donc permettre à tout utilisateur connecté de proposer un produit WooCommerce à la vente. Nous éclaterons la logique en deux parties : l’affichage du formulaire et les actions à faire, à savoir :

  1. validation des données
  2. création du produit WooCommerce
  3. upload des photos (non décrit dans ce tutoriel mais commenté ici)
  4. envoi de notifications e-mail (non décrit dans ce tutoriel mais commenté ici)
  5. redirection de l’utilisateur

Créer un shortcode pour afficher le formulaire de soumission d’un produit dans WordPress

Nous en avions déjà parlé dans un précédent tutoriel et présenté une technique propre pour créer un formulaire WordPress : pensez à séparer le code de son affichage (son code HTML) du code qui le traitera (les actions à effectuer après l’envoi du formulaire). C’est bien plus facile à maintenir sur le long terme !

D’abord, le code…

<?php 

/**
 * Shortcode pour afficher le formulaire pour proposer un produit
 */
add_shortcode('msk_formulaire_proposer_produit', 'msk_shortcode_product_submission');

function msk_shortcode_product_submission($atts) {
	// On ajoute un champ caché pour savoir si c'est un utilisateur lambda ou l'admin qui remplit le formulaire
	$is_admin_hidden_field = (is_user_logged_in() && current_user_can('manage_options')) ? 'on' : 'off';

	// Valeurs par défaut des champs
	$form_values_default = array(
		'product-title' => '',
		'product-description' => ''
	);

	// On boucle pour nettoyer les valeurs, si elles sont renvoyées par le système en cas d'erreur
	$form_values = array_map('sanitize_text_field', wp_parse_args($_GET, $form_values_default));

	$errors = msk_get_current_errors($_GET);

	ob_start(); ?>

	<form id="form-submit-product" class="row" enctype="multipart/form-data" method="post" action="#">
		<?php msk_display_errors($errors); ?>

		<section class="data">
			<fieldset class="affiliate-data">
				<?php if (!is_user_logged_in()) { ?>
				<p>
					<a href="<?php echo get_permalink(get_option('woocommerce_myaccount_page_id')); ?>"><?php _e('Identifiez-vous ou créez un compte afin de gagner des points à chaque vente d\'un de vos produits proposés.'); ?></a>
				</p>
				<?php } else { $user_data = get_userdata(get_current_user_id()); ?>
				<p>
					<?php printf(
						__('Vous êtes connecté en tant que %1$s (e-mail %2$s) : vos points seront reversés sur ce compte parrain.', 'mosaika'),
						'<strong>' . $user_data->user_login . '</strong>',
						'<strong>' . $user_data->user_email . '</strong>'
					); ?>
				</p>
				<?php } ?>
			</fieldset>
			
			<?php if (is_user_logged_in()) { ?>
			<fieldset class="product-data">
				<label for="product-title"><?php _e('Nom du produit', 'mosaika'); ?><span class="required">*</span></label>
				<input type="text" required id="product-title" name="product-title" placeholder="<?php esc_attr_e('Nom du produit', 'mosaika'); ?>" value="<?php esc_attr_e($form_values['product-title']); ?>" />

				<label for="product-description"><?php _e('Description du produit', 'mosaika'); ?><span class="required">*</span></label>
				<textarea id="product-description" name="product-description" placeholder="<?php esc_attr_e('Description du produit', 'mosaika'); ?>" rows="7"><?php echo esc_textarea($form_values['product-description']); ?></textarea>

				<p class="required-text"><span class="required">*</span><?php _e('Champs obligatoires'); ?></p>
			</fieldset>

			<fieldset class="photos">
				<input type="file" id="product-photo" name="product-photo[]" accept="image/jpeg, image/jpg, image/png" multiple class="jfilestyle" data-buttonText="<i class='fa fa-camera'></i> Ajouter une photo" />
			</fieldset>

			<fieldset class="footer">
				<?php wp_nonce_field('msk_submit_product'); ?>
				<input type="hidden" name="is-admin" value="<?php echo $is_admin_hidden_field; ?>" />
				<button class="button" type="submit" name="submit" value="submit-product"><?php _e('Proposer un produit', 'mosaika'); ?></button>
			</fieldset>
			<?php } ?>
		</section>
	</form>

<?php return ob_get_clean();
}

On ne reviendra pas sur la création d’un shortcode, mais analysons le code ci-dessus :

  1. ligne 13, on définit les valeurs par défaut des champs « titre » et « description » du produit. Ici, on les veut vides…
  2. ligne 19, deux choses se passent :
    1. on fusionne nos valeurs par défaut aux valeurs des variables contenues dans l’URL ($_GET) : lorsque l’on traitera le formulaire (un peu plus bas), si les entrées de ce dernier présentent des erreurs, on va renvoyer l’utilisateur sur la page du formulaire avec les valeurs des champs qu’il a remplis dans l’URL. Cela va nous permettre de pré-remplir les champs à nouveau et évitera à l’utilisateur de remplir une seconde fois les champs valides. wp_parse_args() nous permet donc ici de définir un tableau de valeurs par défaut (ligne 13) et de le fusionner avec le tableau des valeurs de l’utilisateur ($_GET)
    2. ensuite, on sécurise et nettoie ces valeurs en appliquant la fonction sanitize_text_field() à chaque valeur du tableau
  3. à partir de la ligne 25, on écrit la structure HTML de notre formulaire :
    1. ligne 30 à 33 : si l’utilisateur n’est pas identifié, on affiche un lien vers la page Mon compte de WooCommerce pour qu’il s’identifie ou créée un compte
    2. ligne 34 à 42 : si il est bien identifié, on affiche un petit message résumant son identité
    3. à partir de la ligne 45 : si il est identifié, on affiche le formulaire avec un champ pour le nom du produit, une zone de texte pour la description du produit et un champ de type file pour uploader une ou plusieurs photos du produit
  4. ligne 61 : on affiche un nonce pour sécuriser notre formulaire contre les attaques CSRF
  5. ligne 62 : on injecte un champ caché is-admin pour savoir si l’utilisateur est administrateur ou non : cela va nous permettre un peu plus tard de ne pas envoyer de notification par e-mail si c’est l’administrateur qui remplit ce formulaire. Evidemment, on va vérifier ce champ dans le traitement des données un peu plus tard, par sécurité.
  6. et ligne 63 : on insère le bouton d’envoi du formulaire. Notez bien son attribut name et value : c’est grâce à lui qu’on va pouvoir intercepter les données du formulaire.

Traiter les données du formulaire WordPress

Maintenant que l’on dispose d’un joli shortcode [msk_formulaire_proposer_produit] pour afficher notre formulaire, il faut désormais s’occuper du traitement des données et des actions à effectuer lorsque ce formulaire WordPress est envoyé.

Intercepter les données envoyées

D’abord, le code qui va nous permettre d’intercepter les données envoyées par le formulaire…

<?php

/**
 * On intercepte les données lorsque le formulaire de proposition d'un produit est soumis par un utilisateur
 */
function msk_process_product_submission() {
	if (isset($_POST['submit']) && $_POST['submit'] == 'submit-product') {
		check_admin_referer('msk_submit_product');

		$data = (!empty($_POST)) ? $_POST : array();

		$data['errors'] = array();

		$data = apply_filters('msk_do_product_submission', $data);
	}
}
add_action('template_redirect', 'msk_process_product_submission');

On en parlait à la ligne 63 du shortcode : c’est le bouton de soumission du formulaire qui va nous envoyer un paramètre submit avec une valeur submit-product. On va donc pouvoir s’infiltrer via le hook template_redirect afin de vérifier si le formulaire a été envoyé par l’utilisateur : ce hook s’exécute avant que les headers de la page ne soient générés par WordPress : parfait pour pouvoir faire tout ce que l’on a à faire puis rediriger l’utilisateur où bon nous semble.

  1. ligne 7 : étant donné que notre fonction msk_process_product_submission() est hookée sur l’action template_redirect de WordPress, notre code risque de s’exécuter à chaque chargement de page de notre site WordPress. Pour éviter ça, on vérifie si c’est l’heure de traiter notre formulaire en regardant si $_POST[‘submit’] existe et si sa valeur est submit-product. Si c’est le cas, c’est que l’utilisateur vient de remplir et envoyer notre formulaire et qu’il est temps pour nous de le recevoir et de le traiter.
  2. ligne 8 : on vérifie la validité du nonce msk_submit_product avec check_admin_referer()
  3. ligne 10 : on créée une variable $data, clone de $_POST, par simple souci de bonne nomenclature
  4. ligne 12 : on ajoute une clé errors dans notre variable $data, pour l’instant un tableau vide
  5. ligne 14 : nous créons un hook maison qui va envoyer la variable $data à tout plein de fonctions que l’on va créer. Plutôt que de créer une-seule-et-unique-grosse-fonction-qui-fait-tout (pour vérifier les données du formulaire, puis créer un produit WooCommerce, puis uploader les photos et les assigner au produit, puis envoyer des notifications e-mail, etc.), on va créer une fonction par action à effectuer. On obtiendra un code bien plus lisible et plus facile à maintenir.
    C’est donc sur le hook (filtre) msk_do_product_submission que nos fonctions vont se hooker pour recevoir, analyser et faire des choses avec les données du formulaire envoyé stockées dans la variable $data.
Schéma explicatif des hooks WordPress
Schéma explicatif de notre filtre pour effectuer nos logiques
What ? Pourquoi un filtre au lieu d’une action ?

Bonne question ! Nous souhaitons « exécuter du code » et non pas nécessairement modifier la variable $data, right ? Il serait donc plus logique de déclarer une action (avec do_action) plutôt qu’un filtre.

La raison est simple : chaque bloc qui va exécuter une action (créer un produit, envoyer un e-mail, etc.) va potentiellement modifier la variable $data pour y ajouter de nouvelles clés/valeurs :

  • la fonction qui va créer le produit va stocker dans $data[‘product’][‘ID’] l’identifiant du produit créé, pour être utilisé par les fonctions suivantes
  • n’importe quelle fonction peut ajouter une clé errors si une erreur surgit, afin d’annuler les fonctions suivantes
  • etc.

Donc chaque fonction va recevoir / potentiellement modifier / puis retourner la variable $data et l’envoyer à la fonction suivante : seul un filtre peut faire cela, car une action ne nous permettra pas de retourner (return) une réponse.

Autrement dit, des actions ne communiquent pas réellement entre elles. Les filtres WordPress, quant à eux, reçoivent en paramètre une variable commune : cette variable « passe d’un filtre à l’autre » afin d’être potentiellement modifiée par chaque filtre. C’est exactement ce qu’il nous faut pour que toutes nos fonctions connaissent le résultat des fonctions précédentes (soit pour ne pas s’exécuter en cas d’erreur, soit pour récupérer de valeurs fraichement calculées).

Maintenant que nous avons un hook où nos données de formulaire sont rendues disponibles, il nous faut désormais hooker d’autres fonctions sur ce filtre pour faire ce que l’on a à faire…

Notez bien le 3è argument numérique de la fonction add_filter : c’est la priorité de la fonction appelée. Autrement dit, une fonction hookée avec une priorité 10 s’exécutera avant une autre fonction hookée avec une prioritée 20. Vous pourrez ainsi définir le bon ordre d’exécution des logiques et vous assurer par exemple que l’on vérifie d’abord les données, puis que l’on créée le produit WooCommerce, puis que l’on uploade ses photos, etc.

Vérifier et préparer les données du formulaire WordPress

Avant de créer notre produit, il est primordial de s’assurer que les données que l’on reçoit sont là, sont au bon format et qu’aucune entourloupe n’est présente.

<?php

/**
 * On valide les données et on prépare un nouveau tableau mieux organisé pour la suite
 */
function msk_preprocess_data_for_product_submission($data) {
	$validation_rules = array(
		'product-title' => array('required'),
		'is-admin' => array('required', 'is_admin')
	);

	// On vérifie les données selon des règles : si il y a des erreurs, on les aura dans $errors
	$errors = msk_validate_data($data, $validation_rules);

	// Si l'utilisateur n'est pas identifié, on ajoute une erreur
	if (!is_user_logged_in()) $errors[] = 'user:not_logged_in';

	if (empty($errors)) {
		// On prépare un nouveau tableau de données, plus organisé
		$new_data['product'] = array(
			'title' => sanitize_text_field($data['product-title']),
			'content' => sanitize_text_field($data['product-description']),
			'product_meta' => array()
		);

		// On prépare la structure des metadonnées du produit
		$new_data['product']['product_meta']['user_submitted'] = 'on';
		// ... l'ID du parrain
		$new_data['product']['product_meta']['commission_user_id'] = get_current_user_id();
		// ... le taux de commission par défaut
		$new_data['product']['product_meta']['commission_rate'] = 5;
		// ... le début de la validité de commission
		$new_data['product']['product_meta']['commission_date_start'] = date('Y-m-d', strtotime('now'));
		// ... la fin de validité de commission
		$new_data['product']['product_meta']['commission_date_end'] = date('Y-m-d', strtotime('+6 months'));

		$data = $new_data;
	}

	$data['errors'] = $errors;

	return $data;
}
add_filter('msk_do_product_submission', 'msk_preprocess_data_for_product_submission', 10, 1);

Explications :

  1. ligne 7 à 13 : on utilise une fonction maison pour s’assurer que certains champs du formulaire respectent certaines règles. Si un champ ne respecte pas une règle qu’on impose (par exemple, le champ product-title n’est pas rempli), on renvoie une erreur (stockée ligne 13) et nous n’irons pas plus loin (condition ligne 18)
  2. ligne 19 à 37 : au contraire, si les données du formulaire respectent les règles qu’on impose, on va se permettre de restructurer un peu nos données pour qu’elles soient plus identifiables dans la suite de notre code. On va donc créer une nouvelle variable $new_data qui est un tableau associatif :
    1. le sous-tableau product contiendra le titre et la description du produit (lignes 21 et 22)
    2. le sous-tableau product_meta du sous-tableau product contiendra les metadonnées du produit relatives à notre système de commissions :
      1. une metadonnée user_submitted nous indiquera que le produit a été proposé par un prescripteur (ligne 27)
      2. une metadonnée commission_user_id va stocker l’identifiant de l’utilisateur connecté, pour que l’on sache qui est le « parrain » de ce produit (ligne 29)
      3. une metadonnée commission_rate va définir la valeur de la commission (ligne 31)
      4. une metadonnée commission_date_start pour définir le début de validité de versement des gains au parrain (ligne 33). On définit cette date comme étant le moment où le produit est soumis.
      5. une metadonnée commission_date_end pour définir la fin de validité de versement des gains au parrain (ligne 35). On la définit à 6 mois plus tard.
  3. et enfin ligne 42 : on renvoie notre nouveau tableau de données $data pour que les autres fonctions hookées à notre filtre msk_do_product_submission puisse y avoir accès. N’oubliez pas ce return $data à la fin de vos filtres : si vous ne renvoyez pas la donnée, le code d’origine ou les futurs filtres n’y auront pas accès et vous casserez tout 🙁

Avec cette première fonction hookée, nous avons validé nos données puis préparé et restructuré la variable $data. Maintenant, nous pouvons créer notre produit WooCommerce dynamiquement.

Créer dynamiquement le produit WooCommerce (état Brouillon)

Une nouvelle fonction msk_create_product_for_product_submission va se hooker à notre filtre et va se charger de créer notre produit (brouillon) WooCommerce.


<?php 

/**
 * On créée un produit WooCommerce au statut 'brouillon'
 */
function msk_create_product_for_product_submision($data) {
	if (empty($data['errors']) && array_key_exists('product', $data)) {
		// On créée le produit
		$product = new WC_Product;
		$product->set_name($data['product']['title']);
		$product->set_description($data['product']['content']);
		$product->set_status('pending');
		$product->save();

		// L'ancienne méthode...
		/*$product_id = wp_insert_post(
			array(
				'post_type' => 'product',
				'post_content' => $data['product']['content'],
				'post_title' => $data['product']['title'],
				'post_status' => 'pending',
				'post_author' => (current_user_can('manage_options')) ? get_current_user_id() : 1,
			)
		);*/

		$product_id = $product->get_id();

		if (0 >= $product_id) {
			// Erreur dans création du produit
			$data['errors'][] = 'cant_create_product';
			
			//var_dump($product_id->get_error_message());			
		} else {
			// Produit bien créé
			$data['product']['ID'] = $product_id;

			// On enregistre les metadonnées du produit
			if ($product && is_array($data['product']['product_meta'])) {
				foreach ($data['product']['product_meta'] as $meta_key => $meta_value) {
					$product->update_meta_data($meta_key, $meta_value);
				}

				$product->save();
			}
		}
	}

	return $data;
}
add_filter('msk_do_product_submission', 'msk_create_product_for_product_submision', 20, 1);

Comme expliqué dans le schéma ci-dessus, chaque fonction hookée va d’abord vérifier si aucune erreur n’est présente dans $data[‘errors’] (cf ligne 7). Si ce tableau n’est pas vide, donc qu’une erreur est présente, rien ne sera fait et la donnée sera renvoyée au prochain filtre, qui fera la même vérification et n’exécutera donc pas sa logique non plus… ainsi de suite jusqu’au dernier filtre hooké.

La logique de création du produit se passe comme suit :

  1. ligne 9 à 13 : on utilise les nouvelles fonctions CRUD de WooCommerce pour créer un nouveau produit et définir son nom, sa description et son état (brouillon)
  2. ligne 26 : on récupère l’identifiant du produit tout fraîchement créé
  3. ligne 28 à 32 : si on arrive pas à créer le produit, on stocke une erreur
  4. à partir de la ligne 33 : sinon…
    1. ligne 35 : on stocke l’ID du produit créé dans la variable $data pour que nos prochaines fonctions hookées puissent connaître l’identifiant du produit créé
    2. ligne 38 à 44 : on enregistre les méta-données du produit que l’on avait préparées auparavant et qui sont stockées dans le tableau $data[‘product’][‘product_meta’]
  5. et pour terminer, on oublie pas de renvoyer la variable $data ligne 48

Rediriger l’utilisateur via PHP

On passe ici la logique d’upload de photos ou d’envoi de notifications e-mail que vous pourrez découvrir sur le GitHub : la logique est la même, on créée de nouvelles fonctions qui viennent se hooker sur le filtre msk_do_product_submission.

Analysons cependant la dernière fonction que l’on va hooker sur notre filtre : la redirection de l’utilisateur.

<?php

/**
 * On redirige vers la page du formulaire
 */
function msk_redirect_after_product_submission($data) {
	if (empty($data['errors'])) {
		// Si pas d'erreur, on redirige vers la page précédente avec ?notice=product_submitted dans l'URL
		$redirect_url = add_query_arg(
			array(
				'notice' => 'product_submitted'
			),
			remove_query_arg(array('product-title', 'product-description', 'is-admin', '_wpnonce', 'errors', 'notice'), wp_get_referer())
		);
	} else {
		// Sinon, on redirige avec ?errors=... dans l'URL
		unset($data['submit']);
		unset($data['_wp_http_referer']);

		$data = array_map('urlencode', array_merge($data, array('errors' => multi_implode(',', $data['errors']))));

		$redirect_url = add_query_arg(
			$data,
			wp_get_referer()
		);
	}

	wp_redirect($redirect_url);
	exit;
}
add_filter('msk_do_product_submission', 'msk_redirect_after_product_submission', 60, 1);

Arrivé à ce stade, on a deux cas de figure :

  1. soit tout s’est bien passé et le tableau $data[‘errors’] est vide,
  2. soit les fonctions précédentes ont rencontré un problème et le tableau $data[‘errors’] n’est pas vide

Si pas d’erreur :

  1. ligne 9 : on créée une variable $redirect_url qui sera notre URL de redirection
  2. ligne 9 à 14 : on ajoute ?notice=product_submitted dans l’URL avec la fonction add_query_arg() qui permettra d’afficher un message de confirmation sur la page du formulaire par exemple
  3. ligne 13 : on retire tout autre argument de l’URL qui pourrait être présent (en cas d’un premier envoi de formulaire qui a fait remonter des erreurs) avec remove_query_arg()
  4. ligne 28 : et on redirige l’utilisateur vers cette URL que l’on vient de construire

Mais si on a eu des erreurs :

  1. ligne 17 et 18 : on supprime les paramètres qu’on ne souhaite pas faire apparaître dans l’URL de redirection
  2. ligne 20 : deux choses se passent :
    1. on enregistre dans l’URL toutes les erreurs qui ont été rencontrés (dans ?errors=…)
    2. on enregistre aussi dans l’URL les valeurs des champs remplis (afin d’avoir ?product-title=XXXX&product_description=YYYYY et pré-remplir les champs du formulaire avec ces valeurs)
  3. ligne 22 à 25 : on construit notre URL de redirection
  4. ligne 28 : et on redirige l’utilisateur vers cette URL que l’on vient de construire

Grâce à wp_get_referer(), on peut accéder dans notre code à l’URL d’origine (la page web contenant le formulaire de soumission de produit). On modifie comme bon nous semble cette URL afin d’indiquer en paramètres $_GET  si tout s’est bien passé (avec notice=product_submitted présent dans l’URL) ou si on a rencontré des erreurs (avec errors=xxx présent dans l’URL).

A l’lheure où j’écris, le plugin n’affiche aucun message sur la page du formulaire si ce dernier a bien été envoyé et qu’un produit brouillon a été créé. A vous de jouer en jouant avec $_GET[‘notice’] 🙂

Envoyer un e-mail au parrain quand le produit est publié

Une fois le produit WooCommerce envoyé, l’administrateur de la boutique e-commerce va recevoir un e-mail l’informant de ce nouvel ajout. Il va pouvoir éditer ce nouveau produit et une fois publié, notre extension va envoyer un e-mail au parrain pour le notifier de la bonne mise en vente de son produit.

<?php

/**
 * Un peu plus tard : lorsque l'admin publie le produit, on envoie un e-mail au parrain
 */
function msk_notify_parrain_when_product_is_published($post_id) {
	$product = wc_get_product($post_id);
	$user_id = $product->get_meta('commission_user_id', true);
	$user_data = get_userdata($user_id);

	if ($user_data) {
		$user_login = $user_data->user_login;
		$user_email = $user_data->user_email;
		$product_title = $product->get_title();
		$product_url = get_permalink($post_id);

		$subject = 'Votre produit est en vente dans notre boutique !';
		$body = sprintf(
			__('Bonjour %1$s, votre produit %2$s est en vente sur <a href="%3$s">%3$s</a>.', 'mosaika'),
			$user_login,
			$product_title,
			$product_url
		);

		wp_mail($user_email, $subject, $body, array('Content-Type: text/html; charset=UTF-8'));
	}
}
add_action('publish_product', 'msk_notify_parrain_when_product_is_published', 10, 1);

Ici, on profite d’un autre hook : l’action WordPress publish_product est exécutée à chaque fois qu’un post de type product est publié. Il nous suffit donc de récupérer les informations du produit et du parrain pour générer un e-mail que l’on enverra au prescripteur.

Dès que l’administrateur publie un produit WooCommerce qui a été proposé par un prescripteur, ce dernier recevra alors un e-mail automatiquement. Pratique non ?


Nous voila donc en possession d’un joli formulaire et de la logique qui traite ses données. Il vous est maintenant possible de le faire évoluer comme bon vous semble pour, par exemple, ajouter des champs, le styliser via CSS ou exécuter d’autres actions quand le formulaire est envoyé (envoyer un SMS ? partager un message sur Slack ? publier le produit directement ?). Voila le résultat de ce formulaire pour un client :

Version du formulaire après stylisation CSS et ajout de quelques nouveaux champs
Version du formulaire après stylisation CSS et ajout de quelques nouveaux champs

C’est bon, nos utilisateurs peuvent nous proposer des produits ! Il faut maintenant pouvoir les récompenser quand un client achète un produit proposé par un parrain.


Vous avez aimé cet article ?

Partagez-le sur vos réseaux sociaux en guise de remerciement :)


2 commentaires

Laisser un commentaire