Gerenciando Assets do Bootstrap no CodeIgniter 4 com Composer

Ao desenvolver aplicações web com CodeIgniter 4 e gerenciar dependências de frontend como o Bootstrap via Composer, surge a necessidade de disponibilizar esses arquivos (CSS, JavaScript, fontes, etc.) para acesso público através das views. A pasta vendor, onde o Composer instala as dependências, não deve ser diretamente acessível pela web por questões de segurança e organização. A melhor prática para lidar com isso no CodeIgniter 4 é utilizando a biblioteca Publisher.

Por que usar a Biblioteca Publisher?

A biblioteca Publisher do CodeIgniter 4 [1] foi projetada especificamente para resolver o desafio de copiar arquivos de bibliotecas instaladas via Composer (ou de qualquer outra origem) para um diretório acessível publicamente, como a pasta public do seu projeto. Suas principais vantagens incluem:

  • Gerenciamento de Versões: Facilita a atualização de dependências, pois você pode simplesmente reexecutar o comando de publicação após uma atualização do Composer.
  • Organização e Segurança: Mantém a pasta vendor protegida e garante que apenas os assets necessários sejam expostos publicamente.
  • Automação: Permite automatizar o processo de cópia de arquivos, integrando-o ao fluxo de trabalho de desenvolvimento e deploy.
  • Flexibilidade: Oferece controle granular sobre quais arquivos e diretórios devem ser copiados e para onde.

Implementação com a Biblioteca Publisher

A seguir, detalhamos os passos para integrar o Bootstrap 5.3.8, instalado via Composer, em suas views do CodeIgniter 4 usando a biblioteca Publisher.

1. Criar uma Classe Publisher Personalizada

É recomendável criar uma classe Publisher personalizada para o Bootstrap. Isso permite que você defina a origem e o destino dos arquivos de forma organizada. Crie um novo arquivo, por exemplo, app/Publishers/BootstrapPublisher.php:

<?php

namespace App\Publishers;

use CodeIgniter\Publisher\Publisher;

class BootstrapPublisher extends Publisher
{
    /**
     * Define o caminho de origem dos assets do Bootstrap.
     * Normalmente, é o diretório `vendor/twbs/bootstrap`.
     *
     * @var string
     */
    protected $source = ROOTPATH . 'vendor/twbs/bootstrap';

    /**
     * Define o caminho de destino dos assets do Bootstrap.
     * Normalmente, é o diretório `public/assets/bootstrap`.
     *
     * @var string
     */
    protected $destination = FCPATH . 'assets/bootstrap';

    public function publish(): bool
    {
        return $this
            ->addPath('dist') // Copia todo o conteúdo da pasta 'dist' do Bootstrap
            ->merge(true); // Mescla os arquivos, sobrescrevendo se existirem
    }
}

Explicação:

  • $source: Aponta para o diretório raiz do pacote Bootstrap dentro da pasta vendor.
  • $destination: Define o diretório onde os arquivos do Bootstrap serão copiados dentro da sua pasta public. Recomenda-se public/assets/bootstrap para manter a organização.
  • publish(): Este método é onde você define quais arquivos ou diretórios serão copiados. No exemplo, addPath('dist') instrui o Publisher a copiar todo o conteúdo da pasta dist do Bootstrap (que contém CSS, JS, etc.). merge(true) garante que os arquivos sejam mesclados e sobrescritos se já existirem, o que é útil para atualizações.

2. Executar o Comando Spark Publish

Após criar a classe Publisher, você pode executar o comando spark publish no terminal para copiar os assets. O CodeIgniter 4 irá descobrir automaticamente sua classe BootstrapPublisher.

php spark publish

Este comando copiará os arquivos do Bootstrap da pasta vendor/twbs/bootstrap/dist para public/assets/bootstrap/dist.

3. Incluir os Assets nas Views

Com os arquivos do Bootstrap agora disponíveis na pasta public/assets/bootstrap/dist, você pode incluí-los em suas views (por exemplo, app/Views/layout.php ou app/Views/welcome_message.php) da seguinte forma:

<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Minha Aplicação CI4 com Bootstrap</title>
    <!-- Bootstrap CSS -->
    <link href="<?= base_url('assets/bootstrap/dist/css/bootstrap.min.css') ?>" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1>Olá, Bootstrap no CodeIgniter 4!</h1>
        <button class="btn btn-primary">Botão de Exemplo</button>
    </div>

    <!-- Bootstrap JS e dependências (Popper.js) -->
    <script src="<?= base_url('assets/bootstrap/dist/js/bootstrap.bundle.min.js') ?>"></script>
</body>
</html>

Utilize a função base_url() do CodeIgniter para gerar os caminhos corretos para seus assets, garantindo que eles funcionem independentemente da URL base da sua aplicação.

Alternativas (Menos Recomendadas)

Embora existam outras maneiras de lidar com assets de bibliotecas, elas são geralmente menos recomendadas:

  • Symlinks (Links Simbólicos): Criar links simbólicos da pasta vendor para public pode funcionar, mas pode ser problemático em alguns ambientes de hospedagem e menos portável.
  • Acesso Direto (via .htaccess): Tentar configurar o servidor web para acessar diretamente a pasta vendor é uma má prática de segurança e não é recomendado.
  • Cópia Manual: Copiar os arquivos manualmente é propenso a erros e inviável para projetos com muitas dependências ou atualizações frequentes.

Conclusão

A biblioteca Publisher é a solução mais robusta e recomendada pelo CodeIgniter 4 para gerenciar assets de bibliotecas instaladas via Composer. Ela oferece uma abordagem automatizada, segura e organizada para disponibilizar seus arquivos de frontend, como o Bootstrap, em suas views, seguindo as melhores práticas do framework.

Referências

[1] Publisher — CodeIgniter 4.7.0 documentation

Script de Reparo Rápido do Windows

O script abaixo executa uma série de verificações e correções automáticas no Windows, incluindo reparo de arquivos do sistema, restauração da imagem do sistema, verificação de disco, reset de rede, limpeza de arquivos temporários, reinstalação de aplicativos nativos e reset dos componentes do Windows Update.

Esse tipo de script é útil para resolver problemas comuns como erros do sistema, falhas de atualização, problemas de rede e lentidão causada por arquivos temporários.


Código do Script

Copie o código abaixo e salve em um arquivo com extensão .bat, por exemplo:

reparo_windows.bat

@echo off
title Reparo rapido do Windows - Emerson
echo Iniciando...

:: 1 - Verificar e reparar arquivos do sistema
sfc /scannow

:: 2 - Restaurar a imagem do Windows
DISM /Online /Cleanup-Image /RestoreHealth

:: 3 - Agendar verificacao do disco (sera executado no reinicio)
chkdsk C: /f /r

:: 4 - Reset de rede completo
ipconfig /flushdns
ipconfig /release
ipconfig /renew
netsh winsock reset
netsh int ip reset

:: 5 - Limpeza de temporarios
del /s /f /q "%TEMP%\*.*" >nul 2>&1
for /d %%i in ("%TEMP%\*") do rd /s /q "%%i" >nul 2>&1

:: 6 - Reinstalar apps nativos
powershell.exe -Command "Get-AppXPackage -AllUsers | Foreach {Try {Add-AppxPackage -DisableDevelopmentMode -Register '$($_.InstallLocation)\AppXManifest.xml'} Catch {}}"

:: 7 - Reset do Windows Update
net stop wuauserv
net stop bits
net stop cryptsvc
ren C:\Windows\SoftwareDistribution SoftwareDistribution.old
ren C:\Windows\System32\catroot2 catroot2.old
net start wuauserv
net start bits
net start cryptsvc

echo Finalizado. Reinicie o PC.
pause

O que o script faz

1. Verificação de arquivos do sistema

  • Executa o sfc /scannow para detectar e reparar arquivos corrompidos do Windows.

2. Reparo da imagem do Windows

  • Utiliza o DISM para restaurar a integridade da imagem do sistema.

3. Verificação do disco

  • Agenda o chkdsk para corrigir erros no disco na próxima reinicialização.

4. Reset completo da rede

  • Limpa DNS, renova IP e redefine as configurações de rede.

5. Limpeza de arquivos temporários

  • Remove arquivos temporários da pasta %TEMP%.

6. Reinstala aplicativos nativos

  • Re-registra todos os aplicativos padrão do Windows via PowerShell.

7. Reset do Windows Update

  • Reinicia os serviços e recria as pastas usadas pelo Windows Update.

Importante

Para que todas as funções funcionem corretamente, execute o arquivo como Administrador:

  1. Clique com o botão direito no arquivo .bat
  2. Selecione Executar como administrador

Ao final do processo, reinicie o computador para que todas as correções sejam aplicadas.

Docker WordPress com MySQL compartilhado

Vantagens desta configuração

  1. MySQL único: economiza recursos, mais fácil de fazer backup
  2. Isolamento: cada WordPress roda em container separado
  3. Escalabilidade: adicionar novos sites é rápido
  4. Segurança: containers não expostos diretamente (apenas localhost)
  5. Manutenção: atualizar WordPress individualmente sem afetar outros

Habilite os módulos necessários no Apache:

a2enmod headers proxy proxy_http rewrite
systemctl restart apache2

1. MySQL compartilhado

Crie /opt/mysql/docker-compose.yml:

services:
  mysql:
    image: mysql:8.0
    container_name: mysql-shared
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_password_forte_aqui
    volumes:
      - ./data:/var/lib/mysql
    ports:
      - "127.0.0.1:3306:3306"
    networks:
      - shared-network

networks:
  shared-network:
    name: shared-network
    driver: bridge

Inicie o MySQL:

cd /opt/mysql
docker compose up -d

Crie os bancos de dados para cada site:

docker exec -it mysql-shared mysql -uroot -p

Dentro do MySQL:

CREATE DATABASE site1_wp;
CREATE DATABASE site2_wp;
CREATE DATABASE site3_wp;

CREATE USER 'site1_user'@'%' IDENTIFIED BY 'senha_site1';
CREATE USER 'site2_user'@'%' IDENTIFIED BY 'senha_site2';
CREATE USER 'site3_user'@'%' IDENTIFIED BY 'senha_site3';

GRANT ALL PRIVILEGES ON site1_wp.* TO 'site1_user'@'%';
GRANT ALL PRIVILEGES ON site2_wp.* TO 'site2_user'@'%';
GRANT ALL PRIVILEGES ON site3_wp.* TO 'site3_user'@'%';

FLUSH PRIVILEGES;
EXIT;

2. WordPress Site 1

Crie /opt/wordpress/site1/docker-compose.yml:

services:
  wordpress:
    image: wordpress:latest
    container_name: wp-site1
    restart: always
    ports:
      - "127.0.0.1:8081:80"
    environment:
      WORDPRESS_DB_HOST: mysql-shared:3306
      WORDPRESS_DB_NAME: site1_wp
      WORDPRESS_DB_USER: site1_user
      WORDPRESS_DB_PASSWORD: senha_site1
    volumes:
      - ./html:/var/www/html
    networks:
      - shared-network

networks:
  shared-network:
    external: true
cd /opt/wordpress/site1
docker compose up -d

3. WordPress Site 2

Crie /opt/wordpress/site2/docker-compose.yml:

services:
  wordpress:
    image: wordpress:latest
    container_name: wp-site2
    restart: always
    ports:
      - "127.0.0.1:8082:80"
    environment:
      WORDPRESS_DB_HOST: mysql-shared:3306
      WORDPRESS_DB_NAME: site2_wp
      WORDPRESS_DB_USER: site2_user
      WORDPRESS_DB_PASSWORD: senha_site2
    volumes:
      - ./html:/var/www/html
    networks:
      - shared-network

networks:
  shared-network:
    external: true
cd /opt/wordpress/site2
docker compose up -d

4. Apache – VirtualHosts separados

Site 1: /etc/apache2/sites-available/site1.conf

<VirtualHost *:80>
    ServerName site1.com.br
    ServerAlias www.site1.com.br

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass        / http://127.0.0.1:8081/
    ProxyPassReverse / http://127.0.0.1:8081/

    RequestHeader set X-Forwarded-Proto "http"
    RequestHeader set X-Forwarded-Host  "%{HTTP_HOST}e"

    ErrorLog  ${APACHE_LOG_DIR}/site1_error.log
    CustomLog ${APACHE_LOG_DIR}/site1_access.log combined
</VirtualHost>

Site 2: /etc/apache2/sites-available/site2.conf

<VirtualHost *:80>
    ServerName site2.com.br
    ServerAlias www.site2.com.br

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass        / http://127.0.0.1:8082/
    ProxyPassReverse / http://127.0.0.1:8082/

    RequestHeader set X-Forwarded-Proto "http"
    RequestHeader set X-Forwarded-Host  "%{HTTP_HOST}e"

    ErrorLog  ${APACHE_LOG_DIR}/site2_error.log
    CustomLog ${APACHE_LOG_DIR}/site2_access.log combined
</VirtualHost>

Ative os sites:

a2ensite site1.conf
a2ensite site2.conf
apache2ctl configtest
systemctl reload apache2

5. SSL para múltiplos sites

certbot --apache -d site1.com.br -d www.site1.com.br
certbot --apache -d site2.com.br -d www.site2.com.br

O Certbot ajusta o VirtualHost automaticamente e adiciona o redirect HTTP→HTTPS.

Depois, atualize o RequestHeader para HTTPS:

RequestHeader set X-Forwarded-Proto "https"

6. Corrigir URLs do WordPress atrás de proxy

No wp-config.php (dentro de ./html/) adicione antes de /* That's all */:

define('WP_HOME',    'https://seudominio.com.br');
define('WP_SITEURL', 'https://seudominio.com.br');

// Necessário para que o WordPress reconheça o proxy
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

7. Verificação rápida

# Containers rodando?
docker compose ps

# Porta 8080 escutando só no localhost?
ss -tlnp | grep 8080

# Apache do host OK?
systemctl status apache2
curl -I http://seudominio.com.br

Comandos úteis

# Ver todos os sites rodando
docker ps

# Logs de um site específico
docker logs wp-site1

# Parar/iniciar um site
cd /opt/wordpress/site1
docker compose down
docker compose up -d

# Backup do MySQL (todos os bancos)
docker exec mysql-shared mysqldump -uroot -p --all-databases > backup-$(date +%F).sql

A edição dos parâmetros do PHP pode ser feita no arquivo .htaccess. Exemplo:

php_value upload_max_filesize 64M
php_value post_max_size 64M
php_value memory_limit 128M
php_value max_execution_time 300
php_value max_input_time 300

Criar partição SWAP no Ubuntu Server 24.04

Vamos usar a forma mais comum e prática usando um arquivo swap:

Criar Swap usando arquivo

1. Verifique se já existe swap:

sudo swapon --show
free -h

2. Crie o arquivo swap (exemplo com 2GB):

sudo fallocate -l 2G /swapfile

3. Defina as permissões corretas:

sudo chmod 600 /swapfile

4. Configure como área de swap:

sudo mkswap /swapfile

5. Ative o swap:

sudo swapon /swapfile

6. Verifique se está ativo:

sudo swapon --show
free -h

7. Torne permanente (sobrevive a reinicializações):

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Ajustes opcionais de desempenho

Configurar swappiness (controla quando usar swap):

# Ver valor atual
cat /proc/sys/vm/swappiness

# Definir para 10 (usa swap menos agressivamente)
sudo sysctl vm.swappiness=10

# Tornar permanente
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf

Tamanho recomendado de swap:

  • RAM ≤ 2GB: 2x o tamanho da RAM
  • RAM 2-8GB: igual ao tamanho da RAM
  • RAM > 8GB: pelo menos 4GB (ou mais se usar hibernação)

Configurar o nome do servidor e prepará-lo para receber um domínio

Usando o Ubuntu Server 24.04 como exemplo, vamos seguir os seguintes passos:

Primeiro, defina um nome apropriado para o servidor:

# Definir o hostname (exemplo: servidor01 ou o nome que preferir)
sudo hostnamectl set-hostname seu-nome-servidor

# Verificar se foi aplicado
hostnamectl

Depois, edite o arquivo /etc/hosts:

sudo nano /etc/hosts

Adicione uma linha como:

127.0.1.1    seu-nome-servidor.seudominio.com.br    seu-nome-servidor

Configurar o PDI 9.4 no macOS (x86)

A versão 9.4 é a última de código aberto e está presente no repositório do brew.

Fazendo a instalação do data-integration pelo brew ou baixando e descompactando o arquivo em pasta local, uma pequena alteração é necessária para fazer o PDI (spoon) abrir.

  • Copie o arquivo “Data Integration.app” para a pasta /Applications
  • Edite o arquivo “/Applications/Data Integration.app/Contents/MacOS/JavaApplicationStub” e corrija o BASE_DIR para apontar para a pasta do data-integration

O aplicativo deverá aparecer no Launchpad mas também pode ser executado via linha de comando:

/Applications/Data\ Integration.app/Contents/MacOS/JavaApplicationStub

Configurar COLLATE e CTYPE no PostgreSQL

Ao instalar um novo serviço de servidor PostgreSQL as opções de COLLATE e CTYPE podem estar fora da configuração ideal.

Não é possível alterar essas opções em bancos já criados. Uma solução é fazer um backup, criar o banco com as opções desejadas e restaurar o banco na sequencia.

Para deixar as opções no estado padrão desejado, execute o script abaixo.

ALTER database template1 is_template=false;

DROP database template1;

CREATE DATABASE template1
WITH OWNER = postgres
   ENCODING = 'UTF8'
   TABLESPACE = pg_default
   LC_COLLATE = 'en_US.UTF-8'
   LC_CTYPE = 'en_US.UTF-8'
   CONNECTION LIMIT = -1
   TEMPLATE template0;

ALTER database template1 is_template=true;

Adicionar script sh como aplicativo no macOS

Podemos transformar qualquer arquivo executável .sh em um app dentro do desktop macOS, incluindo ícone e atalho no dock.

  1. certificar que o arquivo sh possui o atributo de executável
  2. criar um script com o comando [ do shell script “sh ~/pasta/arquivo.sh” ]
  3. salvar o script
  4. exportar o script para formato de arquivo como aplicativo (isso vai gerar um arquivo .app)
  5. abrir a janela de obter informações do arquivo .app gerado
  6. arrastar e soltar o ícone na parte superior esquerda da janela de informações
  7. copiar o arquivo .app para a pasta de aplicativos
  8. abrir o aplicativo e fixar o atalho no dock

Trocando o dono (OWNER) de todas tabelas de um banco de dados PostgreSQL

Eventualmente podemos nos defrontar uma um banco de dados cujas tabelas possuem diferentes donos (alguma falta de padrão no momento da criação).

O script abaixo troca o dono para o desejado. Deve ser executado dentro do banco alvo.

SELECT format(
          'ALTER TABLE public.%I OWNER TO novo_dono',
          table_name
       )
FROM information_schema.tables
WHERE table_schema = 'public'
  AND table_type = 'BASE TABLE' \gexec

Seleção global dos elementos da coluna em tabelas html

Em casos onde temos mais de uma coluna com o recurso de seleção global dentro de uma tabela html, podemos usar o exemplo abaixo para controlar qual elemento foi selecionado.

<th data-sortable="false" width="150px" class="text-danger">
    <input class="form-check-input" type="checkbox" id="chkSelectAllRem" />
    <label for="chkSelectAllRem">Todas/Nenhuma</label>
</th>
<th data-sortable="false" width="150px" class="text-success">
    <input class="form-check-input" type="checkbox" id="chkSelectAllEnc" />
    <label for="chkSelectAllEnc">Todas/Nenhuma</label>
</th>

...

<td>
    <div class="form-check">
        <input class="form-check-input" type="checkbox" name="op_rem[]" value="<?= $row->id_pr_ordens ?>"
               id="op_rem.<?= $row->id_pr_ordens ?>">
        <label class="form-check-label text-danger" for="op_rem.<?= $row->id_pr_ordens ?>">Remover</label>
    </div>
</td>
<td>
    <div class="form-check">
        <input class="form-check-input" type="checkbox" name="op_enc[]" value="<?= $row->id_pr_ordens ?>"
               id="op_enc.<?= $row->id_pr_ordens ?>" />
        <label class="form-check-label text-success" for="op_enc.<?= $row->id_pr_ordens ?>">Encerrar</label>
    </div>
</td>

...

<script>
$("th input[type='checkbox']").on("change", function() {
    // remove seleção de todos checkboxes
    $('input[type="checkbox"]').not(this).prop("checked", false);
    // seleciona todos os elemento da coluna selecionada
    var cb = $(this), // checkbox that was changed
        th = cb.parent(), // get parent th
        col = th.index() + 1; // get column index. note nth-child starts at 1, not zero
    $("tbody td:nth-child(" + col + ") input").prop("checked", this.checked); // select the inputs and [un]check it
});

$(document).ready(function() {
    // uncheck other boxes on the same row
    $(".form-check-input").click(function() {
        $('input[type="checkbox"]').change(function() {
            $(this).closest('tr').find('input[type="checkbox"]').not(this).prop('checked', false);
        });
    });
});
</script>