Como hacer un admin de blog con Seed php - parte 3

blog-post

En el post anterior te estuve mostrando como hice para enviar peticiones de tipo DELETE con jQuery y recibirlas desde el servidor con Seed php. En esta tercer parte vamos a ver como enviar peticiones de tipo POST y PUT.

Además de tratar estos puntos quería comentarte que hice algunos cambios en el código anterior:

-Solucioné algunos problemas al mostrar imágenes en la el listado de posts

-Arregle un problema con el paginado del admin

-Seguí comentando el código fuente, y a esta altura está en un 90% cubierto

Para hacer las peticiones POST creé una nueva ruta en el index.php que se llama adminPost y se encarga de recibir los datos enviados a través de este método, y validar y filtrar los datos y si cumplen las reglas establecidas almacenarlos en la base de datos notificandolo al cliente:


//Función que maneja el admin POST de los posts
$seed->on('/adminPost', function($params) use($seed) {
	//incluyo la clase validadora
	include('core/validator.php');
	
	//uso la función mysqli_real_escape_string pasandole como primer
	//parámetro la conexión a la base de datos y como segundo la cadena a escapar
	//para que las comillas " ' no rompan la consulta
	$title = mysqli_real_escape_string($seed->get('db'), $params['title']);
	//para slug no lo hago porque ya está validado
	$name = $params['slug'];
	$content = mysqli_real_escape_string($seed->get('db'), $params['description']);
	$image = $params['image'];
	$date = $params['date'];
	
	$post_media_title = mysqli_real_escape_string($seed->get('db'), $params['sm_title']);
	$post_media_description = mysqli_real_escape_string($seed->get('db'), $params['sm_description']);
	
	//instancio el validador para asegurarme que
	//se cumplen las reglas de los valores de los inputs.
	//Como parámetro le paso un array que para cada input 
	//tiene como clave su atributo name y como valor un
	//array donde en value se indica el valor del input y
	//en rules las reglas a validar
	$validator = new Validator(array(
		'title' => array(
			'value' => $title,
			'rules' => 'minLength:5|required:true',
		),
		'name' => array(
			'value' => $name,
			'rules' => 'minLength:5|required:true',
		),
		'content' => array(
			'value' => $content,
			'rules' => 'minLength:5|required:true',
		),
		'image' => array(
			'value' => $image,
			'rules' => 'minLength:4|required:true',
		),
		'date' => array(
			'value' => $date,
			'rules' => 'required:true',
		),
		'sm_title' => array(
			'value' => $post_media_title,
			'rules' => 'minLength:5|required:true',
		),
		'sm_description' => array(
			'value' => $post_media_description,
			'rules' => 'minLength:5|required:true',
		),
	));
	
	//con el método run ejecuto el validador que
	//devuelve un booleano
	if($validator->run()){
		//si la validación fué correcta armo
		//la query para crear el registro del
		//post
		
		//fijate que para el nombre de los campos no uso
		//comillas pero si lo hago para sus valores
		$query = "INSERT INTO posts(
			post_date,
			post_title,
			post_name,
			post_image,
			post_content)
			VALUES (
				'$date',
				'$title',
				'$name',
				'$image',
				'$content'
			)";

			//ejecuto la consulta
			$rs = mysqli_query($seed->get('db'), $query);
			//si la ejecución fué exitosa
			if($rs){
				//con mysqli_insert_id obtengo el id del
				//último insert
				$post_id = mysqli_insert_id($seed->get('db'));
				//escribo la consulta que con el id del post
				//guarda el post_media_title, y el post_media_description
				//en la tabla de los datos de social_media
				$query_social_media = "INSERT INTO social_media(
				post_id,
				post_media_title,
				post_media_description)
				VALUES (
					'$post_id',
					'$post_media_title',
					'$post_media_description'
				)";
				
				$rs2 = mysqli_query($seed->get('db'), $query_social_media);
				//si la segunda consulta se ejecuta bien
				if($rs2){
					//creo un array con los datos del post (incluyendo
					//el id del post creado) al cliente para
					//actualizar la vista
					$data = array(
						'id' => $post_id,
						'title' => $title,
						'name' => $name,
						'content' => $content,
						'image' => $image,
						'date' => $date,
						'post_media_title' => $post_media_title,
						'post_media_description' => $post_media_description,
						//mensaje para mostrar desde el cliente
						'message' => 'El post se creó satisfactoriamente',
					);
					
					//para enviar la respuesta al cliente paso un 
					//array a la función json_encode
					//donde devuelvo un código satisfactorio en el 
					//valor del indice res y un mensaje para mostrar
					//en el valor del indice msg
					echo json_encode(array(
						'res' => 'saveOk',
						'msg' => $data,
					));
					//con la función exit() hago que no se muestre
					//nada más a continuación de la respuesta
					//para evitar errores al recibir el json
					exit();
				}else{
					//si se produjo un error al ejecutar la consulta
					//devuelvo un array notificandolo
					echo json_encode(array(
						'res' => 'saveError',
						'msg' => 'Se produjo un error al guardar los datos. Por favor vuelva a intentarlo.',
					));
					exit();
				}
			}else{
					//si se produjo un error al ejecutar la consulta
					//devuelvo un array notificandolo
					echo json_encode(array(
						'res' => 'saveError',
						'msg' => 'Se produjo un error al guardar los datos',
					));
					exit();
			}
	}else{
		//si hubo un error de validación directamente
		//devuelvo los errores del validador(se pueden customizar
		//los textos desde el código de validator.php)
		echo json_encode(array(
			'res'=>'validationError',
			'msg'=> $validator->getErrors(),
		));
		exit();
	}
});
	

En el caso del método PUT hice lo mismo que para POST pero variando las consultas para que modifique los registros ya creados:


//Función que maneja el admin PUT de los posts
$seed->on('/adminPut', function($params) use($seed){
	//incluyo la clase validadora
	include('core/validator.php');
	
	//obtengo el id del post a editar
	$post_id = $params['post_id'];
	//uso la función mysqli_real_escape_string pasandole como primer
	//parámetro la conexión a la base de datos y como segundo la cadena a escapar
	//para que las comillas " ' no rompan la consulta
	$title = mysqli_real_escape_string($seed->get('db'), $params['title']);
	//para slug no lo hago porque ya está validado
	$name = $params['slug'];
	$content = mysqli_real_escape_string($seed->get('db'), $params['description']);
	$image = $params['image'];
	$date = $params['date'];
	
	$post_media_title = mysqli_real_escape_string($seed->get('db'), $params['sm_title']);
	$post_media_description = mysqli_real_escape_string($seed->get('db'), $params['sm_description']);
	
	//instancio el validador para asegurarme que
	//se cumplen las reglas de los valores de los inputs.
	//Como parámetro le paso un array que para cada input 
	//tiene como clave su atributo name y como valor un
	//array donde en value se indica el valor del input y
	//en rules las reglas a validar
	$validator = new Validator(array(
		'title' => array(
			'value' => $title,
			'rules' => 'minLength:5|required:true',
		),
		'name' => array(
			'value' => $name,
			'rules' => 'minLength:5|required:true',
		),
		'content' => array(
			'value' => $content,
			'rules' => 'minLength:5|required:true',
		),
		'image' => array(
			'value' => $image,
			'rules' => 'minLength:4|required:true',
		),
		'date' => array(
			'value' => $date,
			'rules' => 'required:true',
		),
		'sm_title' => array(
			'value' => $post_media_title,
			'rules' => 'minLength:5|required:true',
		),
		'sm_description' => array(
			'value' => $post_media_description,
			'rules' => 'minLength:5|required:true',
		),
	));
	
	//con el método run ejecuto el validador que
	//devuelve un booleano
	if($validator->run()){
		//si la validación fué correcta armo
		//la query para crear el registro del
		//post
		
		//Fijate que para el nombre de los campos no uso
		//comillas pero si lo hago para sus valores.
		//Además hago un solo update para actualizar las 2 tablas
		$query = "UPDATE posts p, social_media sm SET 
					p.post_title = '$title',
					p.post_name = '$name',
					p.post_content = '$content',
					p.post_image = '$image',
					p.post_date = '$date',
					sm.post_media_title = '$post_media_title',
					sm.post_media_description = '$post_media_description'
					WHERE p.ID = $post_id
					AND sm.post_id = $post_id";
		// echo $query;exit();
		//ejecuto la consulta
		$rs = mysqli_query($seed->get('db'), $query);
		//si la ejecución fué exitosa
		if($rs){
			//creo un array con los datos del post (incluyendo
			//el id del post creado) al cliente para
			//actualizar la vista
			$data = array(
				'id' => $post_id,
				'title' => $title,
				'name' => $name,
				'content' => $content,
				'image' => $image,
				'date' => $date,
				'post_media_title' => $post_media_title,
				'post_media_description' => $post_media_description,
				//mensaje para mostrar desde el cliente
				'message' => 'El post se guardó satisfactoriamente',
			);
			
			//para enviar la respuesta al cliente paso un 
			//array a la función json_encode
			//donde devuelvo un código satisfactorio en el 
			//valor del indice res y un mensaje para mostrar
			//en el valor del indice msg
			echo json_encode(array(
				'res' => 'saveOk',
				'msg' => $data,
			));
			//con la función exit() hago que no se muestre
			//nada más a continuación de la respuesta
			//para evitar errores al recibir el json
			exit();
		}else{
			//si se produjo un error al ejecutar la consulta
			//devuelvo un array notificandolo
			echo json_encode(array(
				'res' => 'saveError',
				'msg' => 'Se produjo un error al guardar los datos',
			));
			exit();
		}
	}else{
		//si hubo un error de validación directamente
		//devuelvo los errores del validador(se pueden customizar
		//los textos desde el código de validator.php)
		echo json_encode(array(
			'res'=>'validationError',
			'msg'=> $validator->getErrors(),
		));
		exit();
	}
});

Desde el lado del cliente, tuve que agregar y cambiar algunas funciones para poder enviar los datos:


//recupero la base url del admin que guardé en el atributo 
//data-base-url del <body> en la vista
var baseurl = $('body').attr('data-base-url'),

//guardo un objeto jQuery del contenedor de acciones
//para reusar después
//haciendolo evito tener que buscarlo muchas veces
$actions = $('.actions');

/* Mostrar artículo */
//guardo un objeto jQuery de los artículos
//haciendolo evito tener que buscarlos muchas veces
var $articles = $('.show_article');

//uso un evento diferido pasandole como 
//elemento el contenedor de todos los articles
//y como segundo del método on() el link que quiero
//que escuche el evento click
//de esta manera no solo se aplica a los links que hay
//cargados, sino que también a los que se creen dinámicamente
//al crear un post
$('.container').on('click', 'article .title-wrapper a.show_article' , function(e){
	//con el método preventDefault() llamado
	//desde el evento evito que se ejecute
	//la acción por defecto de un link
	e.preventDefault();
	//con $(this) hago referencia al link
	//sobre el que se hizo click
	var $item = $(this),
	//y a través de él obtengo el 
	//valor de su atributo data-id
	id = $item.attr('data-id')
	post = {};

	// Traigo los datos del post por ajax
	$.ajax({
		//url donde donde se va a enviar la petición
		'url' : baseurl + '/adminGet',
		//uso el type GET porque voy a
		//pedir un recurso del servidor
		'type' : 'GET',
		//voy a esperar una respuesta en formato json
		'dataType' : 'json',
		//como parámetros envío el objeto data
		'data' : {
			'id': id,
		},
		//uso async false para que
		//no empiece la carga de la template
		//hasta que no se hayan traído los
		//datos por ajax
		'async' : false,
		//al recibir la respuesta del servidor se
		//ejecuta la función del parámetro success
		'success' : function(d){
			//como parámetro se espera un objeto json
			//con los datos del servidor
			//en este caso lo almaceno su primer valor
			//en la variable post
			post = d[0];
		},
	});
	
	//si el formulario está abierto lo oculto
	hideForm();
	
	//cargo la plantilla del formulario y la transformo a un
	//objeto jQuery para que después sea más fácil
	//cargar los valores de sus campos
	var articleTpl = $(loadTpl('article')),
	//creo una variable bandera, que inicia 
	//en false y sirve para saber si el código
	//pasó por un lugar específico.
	//En este caso la uso para saber si
	//cambió el valor de algún campo de texto
	article_change = false;
	
	//función que voy a llamar cuando
	//se modifica algún campo del formulario
	//y va a poner en true a la bandera article_change
	//De esta manera voy a saber enviar un ajax para
	//guardar el formulario
	var updateFlag = function(){
		article_change = true;
	};
	
	//función que actualiza automaticamente el valor
	//del slug cuando cambia el del título
	var updateSlug = function(e){
		//desde la propiedad which del evento efecto
		//obtengo el código ASCII de la tecla presionada
		//y con el método fromCharCode lo transformo al
		//caractér que le corresponde. Después con 
		//toLowerCase() lo convierto a minúscula
		var new_key = String.fromCharCode(e.which).toLowerCase();
		//el la nueva tecla presionada es un caractér alfanumerico
		//se lo concateno al slug
		if(new_key.match(/[a-z0-9]/i)){
			var slug = titleToSlug($('#title').val()) + new_key;
		}else{
			var slug = titleToSlug($('#title').val());
		}
		
		//guardo el nuevo valor en el input de id #slug
		$('#slug').val(slug);
	}
	
	//inserto los datos de la consulta en el formulario
	//buscando cada campo y llamando a través de él al método
	//.val() pasandole como parámetro el valor que le corresponda
	articleTpl.find('#post_id')
	//con val() obtengo el valor
	.val(post.ID);
	
	articleTpl.find('#title')
	//con val() obtengo el valor
	.val(post.post_title)
	//en el evento change del input
	//(se ejecuta cuando cambia su valor)
	//se ejecuta la función updateFlag
	.on('change', updateFlag)
	//en el input del título además de
	//actualizar la bandera cuando cambia su valor,
	//cuando se presiona una tecla dentro de él
	//llama a la función updateSlug para que
	//genere automaticamente el fragmento url
	//para ese título
	.on('keydown', updateSlug);
	
	articleTpl.find('#slug')
	.val(post.post_name)
	.on('change', updateFlag);
	
	articleTpl.find('#postdescription')
	.val(post.post_content)
	.on('change', updateFlag);
	
	articleTpl.find('#image')
	.val(post.post_image)
	.on('change', updateFlag);
	
	articleTpl.find('#date_created')
	.val(post.post_date)
	.on('change', updateFlag);
	
	articleTpl.find('#sm_title')
	.val(post.post_media_title)
	.on('change', updateFlag);
	
	articleTpl.find('#sm_description')
	.val(post.post_media_description)
	.on('change', updateFlag);
	
	//vinculo la función anónima que maneja los clicks
	//en el botón de cancelar
	articleTpl.find('.btn-cancel').on('click', function(e){
		//con el método preventDefault() llamado
		//desde el evento evito que se ejecute
		//la acción por defecto de un link
		e.preventDefault();
		//si el formulario está abierto lo oculto
		hideForm();
	});
	
	//vinculo la función anónima que maneja los clicks
	//en el botón de guardar el formulario
	articleTpl.find('.btn-send').on('click', function(e){
		//con el método preventDefault() llamado
		//desde el evento evito que se ejecute
		//la acción por defecto de un link
		e.preventDefault();
		
		//reviso la bandera article_change y si
		//es true significa que se modifico el artículo
		if(article_change){
			//llamo a la función que obtiene los valores
			//del formulario y envía un ajax para guardarlos
			createPost();
		}else{
			//sino muestro un mensaje
			alert('No se registraron cambios en el post');
			//y oculto el formulario
			hideForm();
		}
	});
	
	//agrego la plantilla javascript
	//al final del div contenedor $actions
	$actions.after().append(articleTpl);
	
	//desplazo la pantalla hasta 
	//el comienzo del form
	goToForm();
	
});

/* Crear artículo */

$actions.find('#createPost').on('click',function(e){
	//con el método preventDefault() llamado
	//desde el evento evito que se ejecute
	//la acción por defecto de un link
	e.preventDefault();
	//si el formulario está abierto lo oculto
	hideForm();
	
	var articleTpl = $(loadTpl('article'));
	//cargo la plantilla

	//función que voy a llamar cuando
	//se modifica algún campo del formulario
	//y va a poner en true a la bandera article_change
	//De esta manera voy a saber enviar un ajax para
	//guardar el formulario
	var updateFlag = function(){
		article_change = true;
	};
	
	//función que actualiza automaticamente el valor
	//del slug cuando cambia el del título
	var updateSlug = function(e){
		//desde la propiedad which del evento efecto
		//obtengo el código ASCII de la tecla presionada
		//y con el método fromCharCode lo transformo al
		//caractér que le corresponde. Después con 
		//toLowerCase() lo convierto a minúscula
		var new_key = String.fromCharCode(e.which).toLowerCase();
		//con .val() obtengo el valor del campo title 
		//y se lo paso a la función titleToSlug y a este
		//texto le concateno la nueva tecla presinada
		var slug = titleToSlug($('#title').val()) + new_key;
		//guardo el nuevo valor en el input de id #slug
		$('#slug').val(slug);
	}
	
	articleTpl.find('#title')
	//en el evento change del input
	//(se ejecuta cuando cambia su valor)
	//se ejecuta la función updateFlag
	.on('change', updateFlag)
	//en el input del título además de
	//actualizar la bandera cuando cambia su valor,
	//cuando se presiona una tecla dentro de él
	//llama a la función updateSlug para que
	//genere automaticamente el fragmento url
	//para ese título
	.on('keydown', updateSlug);
	
	articleTpl.find('#slug')
	.on('change', updateFlag);
	
	articleTpl.find('#postdescription')
	.on('change', updateFlag);
	
	articleTpl.find('#image')
	.on('change', updateFlag);
	
	//creo un objeto Date y la fecha en formato ISO string
	//corto esa cadena en el caractér número 19 y remplazo la T 
	//por un espacio
	date = new Date().toISOString().substr(0, 19).replace('T', ' ');
	articleTpl.find('#date_created')
	.val(date);
	
	articleTpl.find('#sm_title')
	.on('change', updateFlag);
	
	articleTpl.find('#sm_description')
	.on('change', updateFlag);
	
	//vinculo los clicks a los botones de guardar y cancelar
	//a las funciones que ejecutan sus acciones
	articleTpl.find('.btn-cancel').on('click', function(e){
		//con el método preventDefault() llamado
		//desde el evento evito que se ejecute
		//la acción por defecto de un link
		e.preventDefault();
		//al hacer click en cancelar
		//oculto el formulario
		hideForm();
	});
	
	articleTpl.find('.btn-send').on('click', function(e){
		//con el método preventDefault() llamado
		//desde el evento evito que se ejecute
		//la acción por defecto de un link
		e.preventDefault();
		//si cambió algún campo del formulario
		if(article_change){
			//llamo a la función que obtiene los valores
			//del formulario y envía un ajax para guardarlos
			createPost();
		}else{
		//sino oculto el formulario
			hideForm();
			alert('No se encontraron datos para crear el post');
		}
	});
	
	//agrego la plantilla javascript
	//al final del div contenedor $actions
	$actions.after().append(articleTpl);
	
	//desplazo la pantalla hasta 
	//el comienzo del form
	goToForm();
});

Para ambos casos llamo a la función createPost(), pero dependiendo si existe el id de post o no en el formulario hago una petición POST o PUT:


var createPost = function(){
	//por una cuestion de orden uso var una sola vez para
	//declarar las variables y después uso coma , para separarlo
	//de las demás declaraciones
	var $form_article = $('#form-article-wrapper'),
	//recupero la base url del admin que guarde en el atributo 
	//data-base-url del <body> en la vista
	baseurl = $('body').attr('data-base-url');
	//Tené en cuenta que el uso de var es importante para que las
	//variables se declaren en el ámbito de la función createPost
	//y no pisen a otras declaradas en el ámbito global window
	
	//si existe el post_id es porque es una edición
	//sino, es un post nuevo.
	//Lo hago con un operador ternario en lugar de if, para que ocupe
	//menos espacio:  expresión_a_evaluar ? true : false;
	var post_id = $('#post_id').val() != '' ? $('#post_id').val() : false;
	
	//creo un objeto clave : valor en el que con .find() 
	//busco cada campo y con .val() su valor
	var data = {
		'title' : $form_article.find('#title').val(),
		'slug' : $form_article.find('#slug').val(),
		'description' : $form_article.find('#postdescription').val(),
		'image' : $form_article.find('#image').val(),
		'date' : $form_article.find('#date_created').val(),
		'sm_title' : $form_article.find('#sm_title').val(),
		'sm_description' : $form_article.find('#sm_description').val(),
	};
	
	//si el post_id es distinto de false la
	//petición va a ser PUT
	if(post_id){
		//lo agrego al objeto de datos a
		//enviar por ajax
		data.post_id = post_id;
		
		//y los mando por ajax al servidor para guardarlos
		$.ajax({
			//url donde donde se va a enviar la petición
			'url' : baseurl + '/adminPut',
			//uso el type PUT porque voy a
			//modificar un recurso del servidor
			'type' : 'PUT',
			//voy a esperar una respuesta en formato json
			'dataType' : 'json',
			//como parámetros envío el objeto data
			'data' : data,
			//al recibir la respuesta del servidor se
			//ejecuta la función del parámetro success
			'success' : function(d){
				//si la edición se pudo
				//llevar a cabo
				if(d.res=='saveOk'){
					//uso console.log() para debuggear
					//los datos de la respuesta
					console.log('Respuesta put:');
					console.log(d);
					alert(d.msg.message);
					//busco el elemento article que le
					//corresponde al actualizado y con
					//find() busco su título para agregarle
					//con .text() el título actualizado
					$('#article-'+d.msg.id)
					.find('.title')
					.text(d.msg.title);
				//si la edición devolvió
				//un error
				}else if(d.res=='saveError'){
					alert(d.msg);
				//si la validación genero un error
				}else{
					alert(d.msg);
				}
			},
		});
	}else{ //sino, la petición es POST
		//y los mando por ajax al servidor para guardarlos
		$.ajax({
			//url donde donde se va a enviar la petición
			'url' : baseurl + '/adminPost',
			//uso el type POST porque voy a
			//crear un recurso en el servidor
			'type' : 'POST',
			//voy a esperar una respuesta en formato json
			'dataType' : 'json',
			//como parámetros envío el objeto data
			'data' : data,
			//al recibir la respuesta del servidor se
			//ejecuta la función del parámetro success
			'success' : function(d){
				console.log('Respuesta post:');
				console.log(d);
				//si la edición se pudo
				//llevar a cabo
				if(d.res=='saveOk'){
					console.log('Respuesta post:');
					console.log(d);
					alert(d.msg.message);
					//busco el contenedor de acciones
					//y con .after() agrego a continuación la estructura
					//html que contiene el nuevo artículo
					$('.actions').after(
						$('<article>', {
							'id' : 'article-'+d.msg.id,
						}).append(
							imgWrapper = $('<div>',{
								'class' : 'img-wrapper',
							}).append(
								$('<img>',{
									'class' : 'transition',
									'src' : baseurl + '/uploads/' + d.msg.image + '.png',
								})
							),
							titleWrapper = $('<div>',{
								'class' : 'title-wrapper',
							}).append(
								$('<a>',{
									'class':'show_article',
									'data-id':d.msg.id,
									'href':'#',
								}).append(
									$('<span>',{
										'class' : 'title',
										'text' : d.msg.title,
									})
								)
							),
							deleteWrapper = $('<div>',{
								'class' : 'delete-wrapper',
							}).append(
								$('<a>', {
									'data-id' : d.msg.id,
									'class' : 'icon-wrapper',
									'href' : '#',
								}).append(
									$('<div>',{
										'class':'icon-article transition-bg-out',
									}).append(
										$('<span>',{
											'class' : 'icon-text',
											'text' : '-',
										})
									),
									$('<div>', {
										'class' : 'text',
										'text' : 'Borrar artículo',
									})
								)
							)
						) //fin article
					);
				//si la edición devolvió
				//un error
				}else if(d.res=='saveError'){
					//muestro el mensaje
					alert(d.msg);
				//si la validación genero un error
				}else{
					//también lo muestro
					alert(d.msg);
				}
			},
		});
	}
};

Por último te dejo el enlace al código fuente completo para que puedas ver y probar. Con esto estan cubiertas las 4 tipos de peticiones y acciones: crear, mostrar, borrar y modificar. Ahora quedaría pulir un poco el orden del código, y crear un sistema de autenticación para que no este sea solo accesible para un usuario administrador.
Espero que te guste. Abrazo grande!