Initial commit
This commit is contained in:
352
skills/symfony-skill/scripts/deploy.sh
Normal file
352
skills/symfony-skill/scripts/deploy.sh
Normal file
@@ -0,0 +1,352 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Symfony Deployment Script
|
||||
# Usage: ./deploy.sh [environment] [branch]
|
||||
# Example: ./deploy.sh production main
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
ENVIRONMENT=${1:-production}
|
||||
BRANCH=${2:-main}
|
||||
PROJECT_PATH="/var/www/symfony-app"
|
||||
BACKUP_PATH="/var/backups/symfony-app"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Functions
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
check_requirements() {
|
||||
log_info "Checking requirements..."
|
||||
|
||||
# Check if PHP is installed
|
||||
if ! command -v php &> /dev/null; then
|
||||
log_error "PHP is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Composer is installed
|
||||
if ! command -v composer &> /dev/null; then
|
||||
log_error "Composer is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Git is installed
|
||||
if ! command -v git &> /dev/null; then
|
||||
log_error "Git is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check PHP version
|
||||
PHP_VERSION=$(php -r "echo PHP_VERSION;")
|
||||
MIN_PHP_VERSION="8.1.0"
|
||||
|
||||
if [ "$(printf '%s\n' "$MIN_PHP_VERSION" "$PHP_VERSION" | sort -V | head -n1)" != "$MIN_PHP_VERSION" ]; then
|
||||
log_error "PHP version must be at least $MIN_PHP_VERSION (current: $PHP_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "All requirements met"
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
log_info "Creating backup..."
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
mkdir -p "$BACKUP_PATH"
|
||||
|
||||
# Backup database
|
||||
if [ -f "$PROJECT_PATH/.env.local" ]; then
|
||||
source "$PROJECT_PATH/.env.local"
|
||||
|
||||
if [ ! -z "$DATABASE_URL" ]; then
|
||||
# Parse database URL
|
||||
DB_USER=$(echo $DATABASE_URL | sed -E 's/.*:\/\/([^:]+):.*/\1/')
|
||||
DB_PASS=$(echo $DATABASE_URL | sed -E 's/.*:\/\/[^:]+:([^@]+)@.*/\1/')
|
||||
DB_HOST=$(echo $DATABASE_URL | sed -E 's/.*@([^:\/]+).*/\1/')
|
||||
DB_NAME=$(echo $DATABASE_URL | sed -E 's/.*\///')
|
||||
|
||||
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$BACKUP_PATH/db_backup_$TIMESTAMP.sql"
|
||||
log_info "Database backup created: db_backup_$TIMESTAMP.sql"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Backup files
|
||||
tar -czf "$BACKUP_PATH/files_backup_$TIMESTAMP.tar.gz" \
|
||||
-C "$PROJECT_PATH" \
|
||||
--exclude=var/cache \
|
||||
--exclude=var/log \
|
||||
--exclude=vendor \
|
||||
--exclude=node_modules \
|
||||
.
|
||||
|
||||
log_info "Files backup created: files_backup_$TIMESTAMP.tar.gz"
|
||||
|
||||
# Keep only last 5 backups
|
||||
ls -1dt "$BACKUP_PATH"/* | tail -n +11 | xargs -r rm -f
|
||||
}
|
||||
|
||||
pull_latest_code() {
|
||||
log_info "Pulling latest code from $BRANCH branch..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Stash any local changes
|
||||
git stash
|
||||
|
||||
# Fetch latest changes
|
||||
git fetch origin
|
||||
|
||||
# Checkout and pull the specified branch
|
||||
git checkout "$BRANCH"
|
||||
git pull origin "$BRANCH"
|
||||
|
||||
log_info "Code updated to latest version"
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
log_info "Installing dependencies..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Install Composer dependencies
|
||||
if [ "$ENVIRONMENT" = "production" ]; then
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
else
|
||||
composer install --optimize-autoloader --no-interaction
|
||||
fi
|
||||
|
||||
# Install NPM dependencies if package.json exists
|
||||
if [ -f "package.json" ]; then
|
||||
npm ci
|
||||
|
||||
# Build assets
|
||||
if [ "$ENVIRONMENT" = "production" ]; then
|
||||
npm run build
|
||||
else
|
||||
npm run dev
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Dependencies installed"
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
log_info "Running database migrations..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Check if there are pending migrations
|
||||
PENDING=$(php bin/console doctrine:migrations:status --show-versions | grep "not migrated" | wc -l)
|
||||
|
||||
if [ "$PENDING" -gt 0 ]; then
|
||||
log_info "Found $PENDING pending migration(s)"
|
||||
|
||||
# Run migrations
|
||||
php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
|
||||
|
||||
log_info "Migrations completed"
|
||||
else
|
||||
log_info "No pending migrations"
|
||||
fi
|
||||
}
|
||||
|
||||
clear_cache() {
|
||||
log_info "Clearing cache..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Clear Symfony cache
|
||||
php bin/console cache:clear --env="$ENVIRONMENT" --no-warmup
|
||||
php bin/console cache:warmup --env="$ENVIRONMENT"
|
||||
|
||||
# Clear OPcache if available
|
||||
if command -v cachetool &> /dev/null; then
|
||||
cachetool opcache:reset
|
||||
log_info "OPcache cleared"
|
||||
fi
|
||||
|
||||
log_info "Cache cleared and warmed up"
|
||||
}
|
||||
|
||||
update_permissions() {
|
||||
log_info "Updating file permissions..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Set proper permissions for var directory
|
||||
chmod -R 775 var/
|
||||
|
||||
# Set proper ownership (adjust user:group as needed)
|
||||
if [ ! -z "$WEB_USER" ]; then
|
||||
chown -R "$WEB_USER":"$WEB_USER" var/
|
||||
fi
|
||||
|
||||
log_info "Permissions updated"
|
||||
}
|
||||
|
||||
run_tests() {
|
||||
log_info "Running tests..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Run PHPUnit tests if they exist
|
||||
if [ -f "bin/phpunit" ] || [ -f "vendor/bin/phpunit" ]; then
|
||||
if [ "$ENVIRONMENT" != "production" ]; then
|
||||
php bin/phpunit --testdox || {
|
||||
log_error "Tests failed! Deployment aborted."
|
||||
exit 1
|
||||
}
|
||||
log_info "All tests passed"
|
||||
else
|
||||
log_warning "Skipping tests in production environment"
|
||||
fi
|
||||
else
|
||||
log_warning "PHPUnit not found, skipping tests"
|
||||
fi
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
log_info "Restarting services..."
|
||||
|
||||
# Restart PHP-FPM
|
||||
if systemctl is-active --quiet php8.1-fpm; then
|
||||
systemctl reload php8.1-fpm
|
||||
log_info "PHP-FPM reloaded"
|
||||
fi
|
||||
|
||||
# Restart web server
|
||||
if systemctl is-active --quiet nginx; then
|
||||
systemctl reload nginx
|
||||
log_info "Nginx reloaded"
|
||||
elif systemctl is-active --quiet apache2; then
|
||||
systemctl reload apache2
|
||||
log_info "Apache reloaded"
|
||||
fi
|
||||
|
||||
# Restart queue workers if Messenger is used
|
||||
if systemctl is-active --quiet symfony-messenger; then
|
||||
systemctl restart symfony-messenger
|
||||
log_info "Messenger workers restarted"
|
||||
fi
|
||||
}
|
||||
|
||||
health_check() {
|
||||
log_info "Performing health check..."
|
||||
|
||||
cd "$PROJECT_PATH"
|
||||
|
||||
# Check if the application responds
|
||||
if [ ! -z "$APP_URL" ]; then
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$APP_URL/health-check")
|
||||
|
||||
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||
log_info "Health check passed (HTTP $HTTP_STATUS)"
|
||||
else
|
||||
log_error "Health check failed (HTTP $HTTP_STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check database connection
|
||||
php bin/console doctrine:query:sql "SELECT 1" > /dev/null 2>&1 || {
|
||||
log_error "Database connection failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_info "All health checks passed"
|
||||
}
|
||||
|
||||
notify_deployment() {
|
||||
MESSAGE="$1"
|
||||
|
||||
# Send notification (configure your notification method)
|
||||
# Example: Slack webhook
|
||||
if [ ! -z "$SLACK_WEBHOOK" ]; then
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data "{\"text\":\"Deployment: $MESSAGE\"}" \
|
||||
"$SLACK_WEBHOOK"
|
||||
fi
|
||||
|
||||
# Log to deployment log
|
||||
echo "[$(date)] $MESSAGE" >> "$PROJECT_PATH/var/log/deployments.log"
|
||||
}
|
||||
|
||||
rollback() {
|
||||
log_error "Deployment failed! Rolling back..."
|
||||
|
||||
# Restore database from backup
|
||||
if [ -f "$BACKUP_PATH/db_backup_$TIMESTAMP.sql" ]; then
|
||||
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$BACKUP_PATH/db_backup_$TIMESTAMP.sql"
|
||||
log_info "Database restored from backup"
|
||||
fi
|
||||
|
||||
# Restore files from backup
|
||||
if [ -f "$BACKUP_PATH/files_backup_$TIMESTAMP.tar.gz" ]; then
|
||||
rm -rf "$PROJECT_PATH"/*
|
||||
tar -xzf "$BACKUP_PATH/files_backup_$TIMESTAMP.tar.gz" -C "$PROJECT_PATH"
|
||||
log_info "Files restored from backup"
|
||||
fi
|
||||
|
||||
clear_cache
|
||||
restart_services
|
||||
|
||||
notify_deployment "❌ Deployment failed and rolled back on $ENVIRONMENT"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Main deployment process
|
||||
main() {
|
||||
log_info "========================================="
|
||||
log_info "Starting Symfony deployment"
|
||||
log_info "Environment: $ENVIRONMENT"
|
||||
log_info "Branch: $BRANCH"
|
||||
log_info "Timestamp: $TIMESTAMP"
|
||||
log_info "========================================="
|
||||
|
||||
# Set error trap
|
||||
trap rollback ERR
|
||||
|
||||
# Execute deployment steps
|
||||
check_requirements
|
||||
create_backup
|
||||
pull_latest_code
|
||||
install_dependencies
|
||||
run_migrations
|
||||
clear_cache
|
||||
update_permissions
|
||||
run_tests
|
||||
restart_services
|
||||
health_check
|
||||
|
||||
# Remove error trap after successful deployment
|
||||
trap - ERR
|
||||
|
||||
log_info "========================================="
|
||||
log_info "Deployment completed successfully!"
|
||||
log_info "========================================="
|
||||
|
||||
notify_deployment "✅ Successfully deployed to $ENVIRONMENT from $BRANCH"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
||||
# Exit successfully
|
||||
exit 0
|
||||
487
skills/symfony-skill/scripts/generate-crud.php
Normal file
487
skills/symfony-skill/scripts/generate-crud.php
Normal file
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Symfony CRUD Generator Script
|
||||
*
|
||||
* Usage: php generate-crud.php EntityName [--api]
|
||||
* Example: php generate-crud.php Product
|
||||
*/
|
||||
|
||||
if ($argc < 2) {
|
||||
echo "Usage: php generate-crud.php EntityName [--api]\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$entityName = $argv[1];
|
||||
$isApi = in_array('--api', $argv);
|
||||
$entityLower = strtolower($entityName);
|
||||
$entityPlural = $entityLower . 's';
|
||||
|
||||
// Generate Controller
|
||||
if (!$isApi) {
|
||||
$controllerCode = "<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\\$entityName;
|
||||
use App\Form\\{$entityName}Type;
|
||||
use App\Repository\\{$entityName}Repository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/$entityPlural')]
|
||||
class {$entityName}Controller extends AbstractController
|
||||
{
|
||||
#[Route('/', name: '{$entityLower}_index', methods: ['GET'])]
|
||||
public function index({$entityName}Repository \$repository): Response
|
||||
{
|
||||
return \$this->render('{$entityLower}/index.html.twig', [
|
||||
'{$entityPlural}' => \$repository->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/new', name: '{$entityLower}_new', methods: ['GET', 'POST'])]
|
||||
public function new(Request \$request, EntityManagerInterface \$entityManager): Response
|
||||
{
|
||||
\${$entityLower} = new $entityName();
|
||||
\$form = \$this->createForm({$entityName}Type::class, \${$entityLower});
|
||||
\$form->handleRequest(\$request);
|
||||
|
||||
if (\$form->isSubmitted() && \$form->isValid()) {
|
||||
\$entityManager->persist(\${$entityLower});
|
||||
\$entityManager->flush();
|
||||
|
||||
\$this->addFlash('success', '$entityName created successfully!');
|
||||
|
||||
return \$this->redirectToRoute('{$entityLower}_show', ['id' => \${$entityLower}->getId()]);
|
||||
}
|
||||
|
||||
return \$this->render('{$entityLower}/new.html.twig', [
|
||||
'{$entityLower}' => \${$entityLower},
|
||||
'form' => \$form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: '{$entityLower}_show', methods: ['GET'])]
|
||||
public function show($entityName \${$entityLower}): Response
|
||||
{
|
||||
return \$this->render('{$entityLower}/show.html.twig', [
|
||||
'{$entityLower}' => \${$entityLower},
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}/edit', name: '{$entityLower}_edit', methods: ['GET', 'POST'])]
|
||||
public function edit(Request \$request, $entityName \${$entityLower}, EntityManagerInterface \$entityManager): Response
|
||||
{
|
||||
\$form = \$this->createForm({$entityName}Type::class, \${$entityLower});
|
||||
\$form->handleRequest(\$request);
|
||||
|
||||
if (\$form->isSubmitted() && \$form->isValid()) {
|
||||
\$entityManager->flush();
|
||||
|
||||
\$this->addFlash('success', '$entityName updated successfully!');
|
||||
|
||||
return \$this->redirectToRoute('{$entityLower}_show', ['id' => \${$entityLower}->getId()]);
|
||||
}
|
||||
|
||||
return \$this->render('{$entityLower}/edit.html.twig', [
|
||||
'{$entityLower}' => \${$entityLower},
|
||||
'form' => \$form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: '{$entityLower}_delete', methods: ['POST'])]
|
||||
public function delete(Request \$request, $entityName \${$entityLower}, EntityManagerInterface \$entityManager): Response
|
||||
{
|
||||
if (\$this->isCsrfTokenValid('delete'.\${$entityLower}->getId(), \$request->request->get('_token'))) {
|
||||
\$entityManager->remove(\${$entityLower});
|
||||
\$entityManager->flush();
|
||||
|
||||
\$this->addFlash('success', '$entityName deleted successfully!');
|
||||
}
|
||||
|
||||
return \$this->redirectToRoute('{$entityLower}_index');
|
||||
}
|
||||
}
|
||||
";
|
||||
} else {
|
||||
// API Controller
|
||||
$controllerCode = "<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Entity\\$entityName;
|
||||
use App\Repository\\{$entityName}Repository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/{$entityPlural}')]
|
||||
class {$entityName}ApiController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface \$entityManager,
|
||||
private SerializerInterface \$serializer,
|
||||
private ValidatorInterface \$validator
|
||||
) {}
|
||||
|
||||
#[Route('', name: 'api_{$entityLower}_index', methods: ['GET'])]
|
||||
public function index({$entityName}Repository \$repository): JsonResponse
|
||||
{
|
||||
\${$entityPlural} = \$repository->findAll();
|
||||
|
||||
return \$this->json(\${$entityPlural}, Response::HTTP_OK, [], [
|
||||
'groups' => ['{$entityLower}:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('', name: 'api_{$entityLower}_create', methods: ['POST'])]
|
||||
public function create(Request \$request): JsonResponse
|
||||
{
|
||||
\${$entityLower} = \$this->serializer->deserialize(
|
||||
\$request->getContent(),
|
||||
$entityName::class,
|
||||
'json'
|
||||
);
|
||||
|
||||
\$errors = \$this->validator->validate(\${$entityLower});
|
||||
|
||||
if (count(\$errors) > 0) {
|
||||
\$errorMessages = [];
|
||||
foreach (\$errors as \$error) {
|
||||
\$errorMessages[\$error->getPropertyPath()] = \$error->getMessage();
|
||||
}
|
||||
|
||||
return \$this->json([
|
||||
'errors' => \$errorMessages
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
\$this->entityManager->persist(\${$entityLower});
|
||||
\$this->entityManager->flush();
|
||||
|
||||
return \$this->json(\${$entityLower}, Response::HTTP_CREATED, [], [
|
||||
'groups' => ['{$entityLower}:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'api_{$entityLower}_show', methods: ['GET'])]
|
||||
public function show($entityName \${$entityLower}): JsonResponse
|
||||
{
|
||||
return \$this->json(\${$entityLower}, Response::HTTP_OK, [], [
|
||||
'groups' => ['{$entityLower}:read', '{$entityLower}:detail']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'api_{$entityLower}_update', methods: ['PUT'])]
|
||||
public function update(Request \$request, $entityName \${$entityLower}): JsonResponse
|
||||
{
|
||||
\$data = json_decode(\$request->getContent(), true);
|
||||
|
||||
\$this->serializer->deserialize(
|
||||
\$request->getContent(),
|
||||
$entityName::class,
|
||||
'json',
|
||||
['object_to_populate' => \${$entityLower}]
|
||||
);
|
||||
|
||||
\$errors = \$this->validator->validate(\${$entityLower});
|
||||
|
||||
if (count(\$errors) > 0) {
|
||||
\$errorMessages = [];
|
||||
foreach (\$errors as \$error) {
|
||||
\$errorMessages[\$error->getPropertyPath()] = \$error->getMessage();
|
||||
}
|
||||
|
||||
return \$this->json([
|
||||
'errors' => \$errorMessages
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
\$this->entityManager->flush();
|
||||
|
||||
return \$this->json(\${$entityLower}, Response::HTTP_OK, [], [
|
||||
'groups' => ['{$entityLower}:read']
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'api_{$entityLower}_delete', methods: ['DELETE'])]
|
||||
public function delete($entityName \${$entityLower}): JsonResponse
|
||||
{
|
||||
\$this->entityManager->remove(\${$entityLower});
|
||||
\$this->entityManager->flush();
|
||||
|
||||
return \$this->json(null, Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
";
|
||||
}
|
||||
|
||||
// Generate Form Type
|
||||
$formCode = "<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\\$entityName;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class {$entityName}Type extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface \$builder, array \$options): void
|
||||
{
|
||||
\$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Name',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'placeholder' => 'Enter name'
|
||||
]
|
||||
])
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'Description',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'rows' => 4,
|
||||
'placeholder' => 'Enter description'
|
||||
]
|
||||
])
|
||||
// Add more fields based on your entity
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver \$resolver): void
|
||||
{
|
||||
\$resolver->setDefaults([
|
||||
'data_class' => $entityName::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
// Generate Templates
|
||||
$baseTemplate = "{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}$entityName Management{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class=\"container mt-4\">
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class=\"alert alert-{{ label }} alert-dismissible fade show\" role=\"alert\">
|
||||
{{ message }}
|
||||
<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}";
|
||||
|
||||
$indexTemplate = "{% extends '{$entityLower}/_base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class=\"d-flex justify-content-between align-items-center mb-4\">
|
||||
<h1>{$entityName} List</h1>
|
||||
<a href=\"{{ path('{$entityLower}_new') }}\" class=\"btn btn-primary\">
|
||||
<i class=\"bi bi-plus-circle\"></i> New $entityName
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class=\"table-responsive\">
|
||||
<table class=\"table table-striped\">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for {$entityLower} in {$entityPlural} %}
|
||||
<tr>
|
||||
<td>{{ {$entityLower}.id }}</td>
|
||||
<td>{{ {$entityLower}.name|default('N/A') }}</td>
|
||||
<td>
|
||||
<a href=\"{{ path('{$entityLower}_show', {'id': {$entityLower}.id}) }}\" class=\"btn btn-sm btn-info\">
|
||||
<i class=\"bi bi-eye\"></i> View
|
||||
</a>
|
||||
<a href=\"{{ path('{$entityLower}_edit', {'id': {$entityLower}.id}) }}\" class=\"btn btn-sm btn-warning\">
|
||||
<i class=\"bi bi-pencil\"></i> Edit
|
||||
</a>
|
||||
<form method=\"post\" action=\"{{ path('{$entityLower}_delete', {'id': {$entityLower}.id}) }}\" style=\"display:inline-block;\" onsubmit=\"return confirm('Are you sure?');\">
|
||||
<input type=\"hidden\" name=\"_token\" value=\"{{ csrf_token('delete' ~ {$entityLower}.id) }}\">
|
||||
<button class=\"btn btn-sm btn-danger\">
|
||||
<i class=\"bi bi-trash\"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan=\"3\" class=\"text-center\">No {$entityPlural} found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}";
|
||||
|
||||
$newTemplate = "{% extends '{$entityLower}/_base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Create New $entityName</h1>
|
||||
|
||||
<div class=\"card\">
|
||||
<div class=\"card-body\">
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<div class=\"mt-3\">
|
||||
<button type=\"submit\" class=\"btn btn-success\">
|
||||
<i class=\"bi bi-check-circle\"></i> Create
|
||||
</button>
|
||||
<a href=\"{{ path('{$entityLower}_index') }}\" class=\"btn btn-secondary\">
|
||||
<i class=\"bi bi-arrow-left\"></i> Back to list
|
||||
</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}";
|
||||
|
||||
$editTemplate = "{% extends '{$entityLower}/_base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Edit $entityName</h1>
|
||||
|
||||
<div class=\"card\">
|
||||
<div class=\"card-body\">
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<div class=\"mt-3\">
|
||||
<button type=\"submit\" class=\"btn btn-primary\">
|
||||
<i class=\"bi bi-save\"></i> Update
|
||||
</button>
|
||||
<a href=\"{{ path('{$entityLower}_show', {'id': {$entityLower}.id}) }}\" class=\"btn btn-secondary\">
|
||||
<i class=\"bi bi-arrow-left\"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\"mt-3\">
|
||||
<form method=\"post\" action=\"{{ path('{$entityLower}_delete', {'id': {$entityLower}.id}) }}\" onsubmit=\"return confirm('Are you sure you want to delete this item?');\">
|
||||
<input type=\"hidden\" name=\"_token\" value=\"{{ csrf_token('delete' ~ {$entityLower}.id) }}\">
|
||||
<button class=\"btn btn-danger\">
|
||||
<i class=\"bi bi-trash\"></i> Delete this $entityName
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}";
|
||||
|
||||
$showTemplate = "{% extends '{$entityLower}/_base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>$entityName Details</h1>
|
||||
|
||||
<div class=\"card\">
|
||||
<div class=\"card-body\">
|
||||
<table class=\"table\">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td>{{ {$entityLower}.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{{ {$entityLower}.name|default('N/A') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ {$entityLower}.description|default('N/A') }}</td>
|
||||
</tr>
|
||||
<!-- Add more fields as needed -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\"mt-3\">
|
||||
<a href=\"{{ path('{$entityLower}_edit', {'id': {$entityLower}.id}) }}\" class=\"btn btn-warning\">
|
||||
<i class=\"bi bi-pencil\"></i> Edit
|
||||
</a>
|
||||
<a href=\"{{ path('{$entityLower}_index') }}\" class=\"btn btn-secondary\">
|
||||
<i class=\"bi bi-arrow-left\"></i> Back to list
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}";
|
||||
|
||||
// Output the generated code
|
||||
echo "===========================================\n";
|
||||
echo "GENERATED SYMFONY CRUD FOR: $entityName\n";
|
||||
echo "===========================================\n\n";
|
||||
|
||||
echo "1. CONTROLLER CODE:\n";
|
||||
echo "-------------------\n";
|
||||
echo $controllerCode;
|
||||
|
||||
echo "\n\n2. FORM TYPE CODE:\n";
|
||||
echo "-------------------\n";
|
||||
echo $formCode;
|
||||
|
||||
if (!$isApi) {
|
||||
echo "\n\n3. TEMPLATES:\n";
|
||||
echo "-------------------\n";
|
||||
echo "Base Template (templates/{$entityLower}/_base.html.twig):\n";
|
||||
echo $baseTemplate;
|
||||
echo "\n\n-------------------\n";
|
||||
echo "Index Template (templates/{$entityLower}/index.html.twig):\n";
|
||||
echo $indexTemplate;
|
||||
echo "\n\n-------------------\n";
|
||||
echo "New Template (templates/{$entityLower}/new.html.twig):\n";
|
||||
echo $newTemplate;
|
||||
echo "\n\n-------------------\n";
|
||||
echo "Edit Template (templates/{$entityLower}/edit.html.twig):\n";
|
||||
echo $editTemplate;
|
||||
echo "\n\n-------------------\n";
|
||||
echo "Show Template (templates/{$entityLower}/show.html.twig):\n";
|
||||
echo $showTemplate;
|
||||
}
|
||||
|
||||
echo "\n\n===========================================\n";
|
||||
echo "INSTALLATION INSTRUCTIONS:\n";
|
||||
echo "===========================================\n";
|
||||
echo "1. Save the controller to: src/Controller/" . ($isApi ? "Api/{$entityName}ApiController.php" : "{$entityName}Controller.php") . "\n";
|
||||
echo "2. Save the form type to: src/Form/{$entityName}Type.php\n";
|
||||
if (!$isApi) {
|
||||
echo "3. Create the template directory: mkdir -p templates/{$entityLower}\n";
|
||||
echo "4. Save each template to its respective file in templates/{$entityLower}/\n";
|
||||
}
|
||||
echo "\n";
|
||||
echo "REQUIRED ENTITY SERIALIZATION GROUPS (for API):\n";
|
||||
echo "Add these to your entity:\n";
|
||||
echo "#[Groups(['{$entityLower}:read'])]\n";
|
||||
echo "#[Groups(['{$entityLower}:write'])]\n";
|
||||
echo "#[Groups(['{$entityLower}:detail'])]\n";
|
||||
echo "\n";
|
||||
echo "Don't forget to:\n";
|
||||
echo "- Ensure your entity exists: src/Entity/$entityName.php\n";
|
||||
echo "- Run migrations if needed: php bin/console doctrine:migrations:migrate\n";
|
||||
echo "- Clear cache: php bin/console cache:clear\n";
|
||||
319
skills/symfony-skill/scripts/generate-entity.php
Normal file
319
skills/symfony-skill/scripts/generate-entity.php
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Symfony Entity Generator Script
|
||||
*
|
||||
* Usage: php generate-entity.php EntityName [field:type:options ...]
|
||||
* Example: php generate-entity.php Product name:string:255 price:decimal:10,2 category:relation:ManyToOne:Category
|
||||
*/
|
||||
|
||||
if ($argc < 2) {
|
||||
echo "Usage: php generate-entity.php EntityName [field:type:options ...]\n";
|
||||
echo "Example: php generate-entity.php Product name:string:255 price:decimal:10,2\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$entityName = $argv[1];
|
||||
$fields = array_slice($argv, 2);
|
||||
|
||||
// Generate entity class
|
||||
$entityCode = "<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\\{$entityName}Repository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: {$entityName}Repository::class)]
|
||||
class $entityName
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int \$id = null;\n\n";
|
||||
|
||||
$gettersSetters = "\n public function getId(): ?int
|
||||
{
|
||||
return \$this->id;
|
||||
}\n";
|
||||
|
||||
foreach ($fields as $fieldDefinition) {
|
||||
$parts = explode(':', $fieldDefinition);
|
||||
$fieldName = $parts[0] ?? '';
|
||||
$fieldType = $parts[1] ?? 'string';
|
||||
$fieldOptions = $parts[2] ?? '';
|
||||
|
||||
if (empty($fieldName)) continue;
|
||||
|
||||
// Handle different field types
|
||||
switch ($fieldType) {
|
||||
case 'string':
|
||||
$length = $fieldOptions ?: '255';
|
||||
$entityCode .= " #[ORM\Column(length: $length)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: $length)]
|
||||
private ?string \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?string
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(string \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
$entityCode .= " #[ORM\Column(type: 'text')]
|
||||
private ?string \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?string
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(?string \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
case 'int':
|
||||
$entityCode .= " #[ORM\Column]
|
||||
#[Assert\NotNull]
|
||||
private ?int \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?int
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(int \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
$precision = '10';
|
||||
$scale = '2';
|
||||
if ($fieldOptions) {
|
||||
$optionParts = explode(',', $fieldOptions);
|
||||
$precision = $optionParts[0] ?? '10';
|
||||
$scale = $optionParts[1] ?? '2';
|
||||
}
|
||||
$entityCode .= " #[ORM\Column(type: 'decimal', precision: $precision, scale: $scale)]
|
||||
#[Assert\NotNull]
|
||||
private ?string \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?string
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(string \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
case 'bool':
|
||||
$entityCode .= " #[ORM\Column]
|
||||
private ?bool \$$fieldName = false;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function is" . ucfirst($fieldName) . "(): ?bool
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(bool \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'datetime':
|
||||
$entityCode .= " #[ORM\Column(type: 'datetime_immutable')]
|
||||
private ?\\DateTimeImmutable \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?\\DateTimeImmutable
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(\\DateTimeImmutable \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
break;
|
||||
|
||||
case 'relation':
|
||||
$relationType = $parts[2] ?? 'ManyToOne';
|
||||
$targetEntity = $parts[3] ?? 'RelatedEntity';
|
||||
|
||||
if ($relationType === 'ManyToOne') {
|
||||
$entityCode .= " #[ORM\ManyToOne(inversedBy: '{$fieldName}s')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?$targetEntity \$$fieldName = null;\n\n";
|
||||
|
||||
$gettersSetters .= "\n public function get" . ucfirst($fieldName) . "(): ?$targetEntity
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function set" . ucfirst($fieldName) . "(?$targetEntity \$$fieldName): static
|
||||
{
|
||||
\$this->$fieldName = \$$fieldName;
|
||||
return \$this;
|
||||
}\n";
|
||||
} elseif ($relationType === 'OneToMany') {
|
||||
$entityCode = str_replace(
|
||||
"use Doctrine\ORM\Mapping as ORM;",
|
||||
"use Doctrine\Common\Collections\ArrayCollection;\nuse Doctrine\Common\Collections\Collection;\nuse Doctrine\ORM\Mapping as ORM;",
|
||||
$entityCode
|
||||
);
|
||||
$entityCode .= " #[ORM\OneToMany(mappedBy: '" . lcfirst($entityName) . "', targetEntity: $targetEntity::class)]
|
||||
private Collection \$$fieldName;\n\n";
|
||||
|
||||
// Add constructor if not present
|
||||
if (!str_contains($gettersSetters, '__construct')) {
|
||||
$gettersSetters .= "\n public function __construct()
|
||||
{
|
||||
\$this->$fieldName = new ArrayCollection();
|
||||
}\n";
|
||||
}
|
||||
|
||||
$gettersSetters .= "\n /**
|
||||
* @return Collection<int, $targetEntity>
|
||||
*/
|
||||
public function get" . ucfirst($fieldName) . "(): Collection
|
||||
{
|
||||
return \$this->$fieldName;
|
||||
}
|
||||
|
||||
public function add" . ucfirst(rtrim($fieldName, 's')) . "($targetEntity \$" . rtrim($fieldName, 's') . "): static
|
||||
{
|
||||
if (!\$this->{$fieldName}->contains(\$" . rtrim($fieldName, 's') . ")) {
|
||||
\$this->{$fieldName}->add(\$" . rtrim($fieldName, 's') . ");
|
||||
\$" . rtrim($fieldName, 's') . "->set" . $entityName . "(\$this);
|
||||
}
|
||||
return \$this;
|
||||
}
|
||||
|
||||
public function remove" . ucfirst(rtrim($fieldName, 's')) . "($targetEntity \$" . rtrim($fieldName, 's') . "): static
|
||||
{
|
||||
if (\$this->{$fieldName}->removeElement(\$" . rtrim($fieldName, 's') . ")) {
|
||||
if (\$" . rtrim($fieldName, 's') . "->get" . $entityName . "() === \$this) {
|
||||
\$" . rtrim($fieldName, 's') . "->set" . $entityName . "(null);
|
||||
}
|
||||
}
|
||||
return \$this;
|
||||
}\n";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$entityCode .= $gettersSetters;
|
||||
$entityCode .= "}\n";
|
||||
|
||||
// Generate repository class
|
||||
$repositoryCode = "<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\\$entityName;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<$entityName>
|
||||
*
|
||||
* @method $entityName|null find(\$id, \$lockMode = null, \$lockVersion = null)
|
||||
* @method $entityName|null findOneBy(array \$criteria, array \$orderBy = null)
|
||||
* @method {$entityName}[] findAll()
|
||||
* @method {$entityName}[] findBy(array \$criteria, array \$orderBy = null, \$limit = null, \$offset = null)
|
||||
*/
|
||||
class {$entityName}Repository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry \$registry)
|
||||
{
|
||||
parent::__construct(\$registry, $entityName::class);
|
||||
}
|
||||
|
||||
public function save($entityName \$entity, bool \$flush = false): void
|
||||
{
|
||||
\$this->getEntityManager()->persist(\$entity);
|
||||
|
||||
if (\$flush) {
|
||||
\$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function remove($entityName \$entity, bool \$flush = false): void
|
||||
{
|
||||
\$this->getEntityManager()->remove(\$entity);
|
||||
|
||||
if (\$flush) {
|
||||
\$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Example custom query methods:
|
||||
|
||||
/**
|
||||
* Find published {$entityName}s ordered by creation date
|
||||
*
|
||||
* @return {$entityName}[]
|
||||
*/
|
||||
public function findPublishedOrderedByDate(): array
|
||||
{
|
||||
return \$this->createQueryBuilder('e')
|
||||
->andWhere('e.published = :published')
|
||||
->setParameter('published', true)
|
||||
->orderBy('e.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find {$entityName}s by search term
|
||||
*
|
||||
* @return {$entityName}[]
|
||||
*/
|
||||
public function findBySearchTerm(string \$term): array
|
||||
{
|
||||
return \$this->createQueryBuilder('e')
|
||||
->andWhere('e.name LIKE :term OR e.description LIKE :term')
|
||||
->setParameter('term', '%' . \$term . '%')
|
||||
->orderBy('e.name', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
echo "Generated Entity Code:\n";
|
||||
echo "======================\n";
|
||||
echo $entityCode;
|
||||
echo "\n\nGenerated Repository Code:\n";
|
||||
echo "==========================\n";
|
||||
echo $repositoryCode;
|
||||
|
||||
echo "\n\nTo use this code:\n";
|
||||
echo "1. Save the entity code to: src/Entity/$entityName.php\n";
|
||||
echo "2. Save the repository code to: src/Repository/{$entityName}Repository.php\n";
|
||||
echo "3. Run: php bin/console make:migration\n";
|
||||
echo "4. Run: php bin/console doctrine:migrations:migrate\n";
|
||||
Reference in New Issue
Block a user