¿Cómo organizar el código en el archivo functions.php de tu tema WordPress?

6 sept 2010, 11:38:45
Vistas: 79.4K
Votos: 109

Cuanto más personalizo WordPress, más empiezo a pensar si debería estar organizando este archivo o dividiéndolo.

Más específicamente, si tengo un montón de funciones personalizadas que solo se aplican al área de administración y otras que solo se aplican a mi sitio web público, ¿hay alguna razón para posiblemente incluir todas las funciones de administración dentro de su propio archivo o agruparlas?

¿Dividirlas en archivos separados o agruparlas podría posiblemente acelerar un sitio web WordPress, o WordPress/PHP automáticamente omite las funciones que tienen un prefijo is_admin en el código?

¿Cuál es la mejor manera de manejar un archivo de funciones grande (el mío tiene 1370 líneas de largo)?

0
Todas las respuestas a la pregunta 8
7
142

Si estás llegando al punto en que el código en el archivo functions.php de tu tema comienza a abrumarte, definitivamente diría que estás listo para considerar dividirlo en múltiples archivos. En este punto, tiendo a hacerlo casi por instinto.

Usa archivos include en el archivo functions.php de tu tema

Yo creo un subdirectorio llamado "includes" dentro de mi directorio de tema y segmento mi código en archivos include organizados según lo que tenga sentido para mí en ese momento (lo que significa que constantemente estoy refactorizando y moviendo código a medida que el sitio evoluciona). También rara vez pongo código real en functions.php; todo va en los archivos include; es simplemente mi preferencia.

Para darte un ejemplo, aquí está mi instalación de prueba que uso para probar mis respuestas a preguntas aquí en WordPress Answers. Cada vez que respondo una pregunta, guardo el código por si lo necesito de nuevo. Esto no es exactamente lo que harías para un sitio en vivo, pero muestra la mecánica de dividir el código:

<?php 
/*
 * functions.php
 * 
 */
require_once( __DIR__ . '/includes/null-meta-compare.php');
require_once( __DIR__ . '/includes/older-examples.php');
require_once( __DIR__ . '/includes/wp-admin-menu-classes.php');
require_once( __DIR__ . '/includes/admin-menu-function-examples.php');

// WA: Adding a Taxonomy Filter to Admin List for a Custom Post Type?
// http://wordpress.stackexchange.com/questions/578/
require_once( __DIR__ . '/includes/cpt-filtering-in-admin.php'); 
require_once( __DIR__ . '/includes/category-fields.php');
require_once( __DIR__ . '/includes/post-list-shortcode.php');
require_once( __DIR__ . '/includes/car-type-urls.php');
require_once( __DIR__ . '/includes/buffer-all.php');
require_once( __DIR__ . '/includes/get-page-selector.php');

// http://wordpress.stackexchange.com/questions/907/
require_once( __DIR__ . '/includes/top-5-posts-per-category.php'); 

// http://wordpress.stackexchange.com/questions/951/
require_once( __DIR__ . '/includes/alternate-category-metabox.php');  

// http://lists.automattic.com/pipermail/wp-hackers/2010-August/034384.html
require_once( __DIR__ . '/includes/remove-status.php');  

// http://wordpress.stackexchange.com/questions/1027/removing-the-your-backup-folder-might-be-visible-to-the-public-message-generate
require_once( __DIR__ . '/includes/301-redirects.php');  

O crea plugins

Otra opción es comenzar a agrupar tu código por función y crear tus propios plugins. Personalmente, empiezo codificando en el archivo functions.php del tema y para cuando termino de desarrollar el código, ya he movido la mayor parte a plugins.

Sin embargo, NO hay ganancia significativa de rendimiento por la organización del código PHP

Por otro lado, estructurar tus archivos PHP es 99% para crear orden y mantenibilidad y 1% por rendimiento, si acaso (organizar archivos .js y .css que el navegador llama mediante HTTP es un caso completamente diferente y tiene implicaciones enormes en el rendimiento). Pero cómo organices tu código PHP en el servidor prácticamente no importa desde una perspectiva de rendimiento.

Y la organización del código es preferencia personal

Y por último pero no menos importante, la organización del código es cuestión de preferencia personal. A algunas personas les disgustaría cómo organizo el código, así como a mí podría no gustarme cómo lo hacen ellos. Encuentra algo que te guste y quédate con ello, pero permite que tu estrategia evolucione con el tiempo a medida que aprendes más y te sientes más cómodo con ella.

6 sept 2010 13:38:17
Comentarios

Buena respuesta, acabo de llegar a este punto donde necesito dividir el archivo de funciones. ¿Cuándo crees que es útil pasar de functions.php a un plugin? Dijiste en tu respuesta: para cuando tengo el código desarrollado ya he movido la mayor parte de mi código a plugins. No lo entiendo del todo, ¿qué quieres decir con desarrollado.

Saif Bechan Saif Bechan
16 ago 2011 10:02:34

+1 por "o crear plugins". Más específicamente, "plugins de funcionalidad"

Ian Dunn Ian Dunn
19 abr 2012 00:29:49

usar rutas relativas podría no ser confiable en todo tipo de configuraciones, siempre se debe usar la ruta absoluta en su lugar

Mark Kaplun Mark Kaplun
22 sept 2016 18:27:10

@MarkKaplun - Tienes absolutamente razón. Desde que escribí esta respuesta aprendí esa lección por las malas. Voy a actualizar mi respuesta. Gracias por señalarlo.

MikeSchinkel MikeSchinkel
23 sept 2016 05:00:40

Recibo "Use of undefined constant DIR - assumed 'DIR' en C:\wamp\www\site\wp-content\themes\mytheme\functions.php" - PHP v5.6.25 y PHP v7.0.10 - No puedo formatear correctamente este DIR en el comentario (subrayadosubrayadoDIRsubrayadosubrayado), pero funciona con dirname(subrayadosubrayadoFILEsubrayadosubrayado)

Marko Marko
1 nov 2016 10:08:45

Precaución: necesitarías usar __DIR__ en lugar de __DIR___, de lo contrario obtendrás un Error Interno del Servidor (500).

robro robro
13 feb 2017 16:01:46

@robro - ¡Ah, gracias por detectarlo! (Malditos errores tipográficos. Grrr...) :-)

MikeSchinkel MikeSchinkel
14 feb 2017 00:47:34
Mostrar los 2 comentarios restantes
0
58

Respuesta tardía

Cómo incluir tus archivos correctamente:

function wpse1403_bootstrap()
{
    // Aquí cargamos desde nuestro directorio de includes
    // Esto considera tanto temas padres como hijos    
    locate_template( array( 'inc/foo.class.php' ), true, true );
}
add_action( 'after_setup_theme', 'wpse1403_bootstrap' );

Lo mismo funciona también en plugins.

Cómo obtener la ruta o URI correcta

También revisa las funciones de la API del sistema de archivos como:

  • home_url()
  • plugin_dir_url()
  • plugin_dir_path()
  • admin_url()
  • get_template_directory()
  • get_template_directory_uri()
  • get_stylesheet_directory()
  • get_stylesheet_directory_uri()
  • etc.

Cómo reducir el número de include/require

Si necesitas obtener todos los archivos de un directorio usa:

foreach ( glob( 'path/to/folder/*.php' ) as $file )
    include $file;

Ten en cuenta que esto ignora fallos (quizás bueno para producción)/archivos no cargables.

Para alterar este comportamiento puedes usar una configuración diferente durante el desarrollo:

$files = ( defined( 'WP_DEBUG' ) AND WP_DEBUG )
    ? glob( 'path/to/folder/*.php', GLOB_ERR )
    : glob( 'path/to/folder/*.php' )

foreach ( $files as $file )
    include $file;

Edición: Enfoque OOP/SPL

Al volver y ver que esta respuesta recibe más votos, pensé en mostrar cómo lo hago actualmente - en un mundo PHP 5.3+. El siguiente ejemplo carga todos los archivos de una subcarpeta del tema llamada src/. Aquí es donde tengo mis bibliotecas que manejan ciertas tareas como menús, imágenes, etc. Ni siquiera necesitas preocuparte por el nombre ya que cada archivo se carga. Si tienes otras subcarpetas en este directorio, se ignoran.

El \FilesystemIterator es el sucesor en PHP 5.3+ del \DirectoryIterator. Ambos son parte del SPL de PHP. Mientras que PHP 5.2 permitía desactivar la extensión SPL (menos del 1% de las instalaciones lo hacía), el SPL ahora es parte del núcleo de PHP.

<?php

namespace Theme;

$files = new \FilesystemIterator( __DIR__.'/src', \FilesystemIterator::SKIP_DOTS );
foreach ( $files as $file )
{
    /** @noinspection PhpIncludeInspection */
    ! $files->isDir() and include $files->getRealPath();
}

Anteriormente, cuando aún soportaba PHP 5.2.x, usaba la siguiente solución: Un \FilterIterator en el directorio src/Filters para obtener solo archivos (y no punteros de carpetas) y un \DirectoryIterator para hacer el bucle y la carga.

namespace Theme;

use Theme\Filters\IncludesFilter;

$files = new IncludesFilter( new \DirectoryIterator( __DIR__.'/src' ) );
foreach ( $files as $file )
{
    include_once $files->current()->getRealPath();
}

El \FilterIterator era tan simple como esto:

<?php

namespace Theme\Filters;

class IncludesFilter extends \FilterIterator
{
    public function accept()
    {
        return
            ! $this->current()->isDot()
            and $this->current()->isFile()
            and $this->current()->isReadable();
    }
}

Además de que PHP 5.2 está muerto/EOL (y 5.3 también), está el hecho de que es más código y un archivo más en el juego, así que no hay razón para usar el segundo y soportar PHP 5.2.x.

Resumiendo

EDIT La forma obviamente correcta es usar código con namespace, preparado para autoloading PSR-4 al poner todo en el directorio apropiado que ya está definido por el namespace. Luego solo usa Composer y un composer.json para gestionar tus dependencias y deja que construya automáticamente tu autoloader PHP (que importa automáticamente un archivo al llamar use \<namespace>\ClassName). Este es el estándar de facto en el mundo PHP, la forma más fácil de proceder y aún más automatizada y simplificada por WP Starter.

13 oct 2012 16:53:12
0

Me gusta utilizar una función para los archivos dentro de una carpeta. Este enfoque facilita añadir nuevas funcionalidades al agregar nuevos archivos. Pero siempre escribo en clases o con namespaces - esto da más control sobre el Namespace de funciones, métodos, etc.

A continuación un pequeño ejemplo; pero también se puede usar con el convenio sobre class*.php

public function __construct() {

    $this->load_classes();
}

/**
 * Retorna un array de características,
 * también escanea el subfolder "/classes" del plugin
 *
 * @since   0.1
 * @return  void
 */
protected function load_classes() {

    // carga todos los archivos con el patrón class-*.php del directorio classes
    foreach( glob( dirname( __FILE__ ) . '/classes/class-*.php' ) as $class )
        require_once $class;

}

En los Themes uso frecuentemente otro escenario. Defino la función del archivo externo en un ID de soporte, como se ve en el ejemplo. Esto es útil si quiero desactivar fácilmente la funcionalidad del archivo externo. Utilizo la función del núcleo de WP require_if_theme_supports() que solo carga si el ID de soporte está activo. En el siguiente ejemplo definí este ID de soporte en la línea antes de cargar el archivo.

    /**
     * Añade soporte para el Personalizador de Tema
     * 
     * @since  09/06/2012
     */
    add_theme_support( 'documentation_customizer', array( 'all' ) );
    // Incluye el personalizador de tema para opciones de configuración, si el tema lo soporta
    require_if_theme_supports( 
        'documentation_customizer',
        get_template_directory() . '/inc/theme-customize.php'
    );

Puedes ver más sobre esto en el repositorio de este tema.

4 oct 2012 12:16:58
9

En términos de dividirlo, en mi plantilla base uso una función personalizada para buscar una carpeta llamada functions en el directorio del tema. Si no existe, la crea. Luego crea un array con todos los archivos .php que encuentra en esa carpeta (si hay alguno) y ejecuta un include(); en cada uno de ellos.

De esta manera, cada vez que necesito escribir alguna funcionalidad nueva, simplemente agrego un archivo PHP a la carpeta functions, y no tengo que preocuparme por codificarlo en el sitio.

<?php
/* 
FUNCIONES para incluir automáticamente documentos php desde la carpeta functions.
*/
//si se ejecuta en php4, crea una función scandir
if (!function_exists('scandir')) {
  function scandir($directory, $sorting_order = 0) {
    $dh = opendir($directory);
    while (false !== ($filename = readdir($dh))) {
      $files[] = $filename;
    }
    if ($sorting_order == 0) {
      sort($files);
    } else {
      rsort($files);
    }
    return ($files);
  }
}
/*
* esta función devuelve la ruta a la carpeta functions.
* Si la carpeta no existe, la crea.
*/
function get_function_directory_extension($template_url = FALSE) {
  //obtiene la url de la plantilla si no se pasa
  if (!$template_url)$template_url = get_bloginfo('template_directory');


  //reemplaza barras con guiones para explode
  $template_url_no_slash = str_replace('/', '.', $template_url);

  //crea un array desde la URL
  $template_url_array = explode('.', $template_url_no_slash);

  //--divide el array

  //Calcula el offset (solo necesitamos los últimos tres niveles)
  //Necesitamos hacer esto para obtener el directorio correcto, no el pasado por el servidor, ya que scandir no funciona cuando hay alias involucrados.
  $offset = count($template_url_array) - 3;

  //divide el array, manteniendo solo hasta la carpeta raíz de WP (donde vive wp-config.php, desde donde se ejecuta el front end)
  $template_url_array = array_splice($template_url_array, $offset, 3);
  //vuelve a unir como string
  $template_url_return_string = implode('/', $template_url_array);
  fb::log($template_url_return_string, 'Template'); //firephp

  //crea el directorio de trabajo actual con la extensión de la plantilla y el directorio functions    
  //si es admin, cambia fuera de la carpeta admin antes de almacenar el directorio de trabajo, luego vuelve a cambiar.
  if (is_admin()) {
    $admin_directory = getcwd();
    chdir("..");
    $current_working_directory = getcwd();
    chdir($admin_directory);
  } else {
    $current_working_directory = getcwd();
  }
  fb::log($current_working_directory, 'Directory'); //firephp

  //método alternativo si chdir no funciona en tu servidor (algunos servidores windows podrían no soportarlo)
  //if (is_admin()) $current_working_directory = str_replace('/wp-admin','',$current_working_directory);

  $function_folder = $current_working_directory . '/' . $template_url_return_string . '/functions';


  if (!is_dir($function_folder)) mkdir($function_folder); //crea la carpeta, si no existe ya (perezoso, pero útil... más o menos)
  //devuelve la ruta
  return $function_folder;

}

//elimina elementos del array que no tienen extensión .php
function only_php_files($scan_dir_list = false) {
  if (!$scan_dir_list || !is_array($scan_dir_list)) return false; //si no se pasa el elemento, o no es un array, sale de la función.
  foreach ($scan_dir_list as $key => $value) {
    if (!strpos($value, '.php')) {

      unset($scan_dir_list[$key]);
    }
  }
  return $scan_dir_list;
}
//ejecuta las funciones para crear la carpeta functions, seleccionarla,
//escanearla, filtrar solo documentos PHP y luego incluirlos en functions

add_action('wp_head', fetch_php_docs_from_functions_folder(), 1);
function fetch_php_docs_from_functions_folder() {

  //obtiene el directorio functions
  $functions_dir = get_function_directory_extension();
  //escanea el directorio y elimina documentos no PHP
  $all_php_docs = only_php_files(scandir($functions_dir));

  //incluye documentos PHP
  if (is_array($all_php_docs)) {
    foreach ($all_php_docs as $include) {
      include($functions_dir . '/' . $include);
    }
  }

}
6 sept 2010 16:50:06
Comentarios

@mildfuzz: Buen truco. Personalmente no lo usaría para código de producción porque hace en cada carga de página lo que podríamos hacer fácilmente una sola vez al lanzar el sitio. También añadiría alguna forma de omitir archivos, como no cargar nada que empiece con guión bajo para poder seguir almacenando trabajos en progreso en el directorio del tema. Por lo demás, ¡buen trabajo!

MikeSchinkel MikeSchinkel
6 sept 2010 22:55:15

Me encanta la idea pero coincido en que esto podría generar carga innecesaria en cada solicitud. ¿Alguna idea sobre si habría una forma sencilla de tener el archivo functions.php final generado automáticamente en caché con algún tipo de actualización cuando se añadan nuevos archivos o en intervalos de tiempo específicos?

NetConstructor.com NetConstructor.com
7 sept 2010 07:27:51

Está bien pero lleva a inflexibilidades, también ¿qué pasa si un atacante logra colocar su código ahí? ¿Y si el orden de los includes es importante?

Tom J Nowell Tom J Nowell
7 sept 2010 15:09:23

@MikeSchinkel Simplemente llamo a mis archivos de trabajo foo._php, luego elimino el _php cuando quiero que se ejecute.

Mild Fuzz Mild Fuzz
10 sept 2010 18:25:32

@NetConstructor: También estaría interesado en alguna solución.

kaiser kaiser
1 feb 2011 09:35:25

@kaiser, Supongo que podrías hacerlo con scripts cron ejecutando una función que realice la búsqueda de carpetas mencionada pero escribiendo los resultados en una base de datos/archivo de texto, luego basando las cargas en esa función. Esto podría potencialmente llevar a que trabajo sin terminar también se incluya en la carga.

Mild Fuzz Mild Fuzz
1 feb 2011 11:19:33

@MildFuzz: Mike acaba de señalarme la API de transients. Tal vez eso podría ser parte de algún tipo de solución...

kaiser kaiser
1 feb 2011 20:52:51

basado en la solución descrita por @mildfuzz - ¿cuál creen que sería el mejor método para excluir automáticamente cualquier archivo o carpeta (y sus subarchivos/subcarpetas) de ser incluidos dentro de su enfoque de auto-inclusión? Mi idea sería usar el enfoque de guión bajo como prefijo. ¿Qué código adecuado sería el mejor enfoque para incluir tales capacidades?

NetConstructor.com NetConstructor.com
10 oct 2012 14:10:37

solo necesitas cambiar if (!strpos($value, '.php')) para incluir cualquier sistema que elijas.

Mild Fuzz Mild Fuzz
10 oct 2012 19:13:14
Mostrar los 4 comentarios restantes
2

Gestiono un sitio con alrededor de 50 tipos de páginas personalizadas únicas en varios idiomas diferentes sobre una instalación en red. Junto con una TONELADA de plugins.

En algún momento nos vimos obligados a dividirlo todo. Un archivo de funciones con 20-30 mil líneas de código no es nada divertido.

Decidimos refactorizar completamente todo el código para gestionar mejor la base de código. La estructura de temas predeterminada de WordPress es buena para sitios pequeños, pero no para sitios más grandes.

Nuestro nuevo functions.php solo contiene lo necesario para iniciar el sitio, pero nada que pertenezca a una página específica.

El diseño de tema que usamos ahora es similar al patrón de diseño MCV, pero en un estilo de codificación procedural.

Por ejemplo, nuestra página de miembros:

page-member.php. Responsable de inicializar la página. Llama a las funciones AJAX correctas o similares. Podría ser equivalente a la parte del Controlador en el estilo MCV.

functions-member.php. Contiene todas las funciones relacionadas con esta página. También se incluye en varias otras páginas que necesitan funciones para nuestros miembros.

content-member.php. Prepara los datos para HTML. Podría ser equivalente al Modelo en MCV.

layout-member.php. La parte HTML.

Después de realizar estos cambios, el tiempo de desarrollo se ha reducido fácilmente en un 50% y ahora el dueño del producto tiene problemas para darnos nuevas tareas. :)

4 oct 2012 12:37:17
Comentarios

Para hacer esto más útil, podrías considerar mostrar cómo funciona realmente este patrón MVC.

kaiser kaiser
5 oct 2012 13:57:01

También me gustaría ver un ejemplo de tu enfoque, preferiblemente con algunos detalles/varias situaciones. El enfoque suena muy interesante. ¿Has comparado la carga/rendimiento del servidor con la metodología estándar que otros usan? Por favor, proporciona un ejemplo en GitHub si es posible.

NetConstructor.com NetConstructor.com
10 oct 2012 14:29:52
0

Desde el archivo functions.php del tema hijo:

    require_once( get_stylesheet_directory() . '/inc/custom.php' );
20 oct 2013 21:34:23
1

En functions.php, una forma más elegante de llamar a un archivo requerido sería:

require_once locate_template('/inc/functions/shortcodes.php');

4 oct 2012 10:23:45
Comentarios

locate_template() tiene un tercer parámetro …

fuxia fuxia
4 oct 2012 10:38:12
0

Combiné las respuestas de @kaiser y @mikeschinkel.

Tengo todas mis personalizaciones para mi tema en una carpeta /includes y dentro de esa carpeta tengo todo organizado en subcarpetas.

Solo quiero que /includes/admin y sus contenidos se incluyan cuando true === is_admin()

Si una carpeta es excluida en iterator_check_traversal_callback al devolver false, entonces sus subdirectorios no serán iterados (ni pasados a iterator_check_traversal_callback)

/**
 *  Requerir todas las personalizaciones bajo /includes
 */
$includes_import_root = 
    new \RecursiveDirectoryIterator( __DIR__ . '/includes', \FilesystemIterator::SKIP_DOTS );

function iterator_check_traversal_callback( $current, $key, $iterator ) {
    $file_name = $current->getFilename();

    // Solo incluir archivos *.php
    if ( ! $current->isDir() ) {
        return preg_match( '/^.+\.php$/i', $file_name );
    }

    // No incluir la carpeta /includes/admin cuando esté en el sitio público
    return 'admin' === $file_name
        ? is_admin()
        : true;
}

$iterator_filter = new \RecursiveCallbackFilterIterator(
    $includes_import_root, 'iterator_check_traversal_callback'
);

foreach ( new \RecursiveIteratorIterator( $iterator_filter ) as $file ) {
    include $file->getRealPath();
}
19 ago 2018 22:02:26