Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:59:04 +08:00
commit cb040b113c
17 changed files with 5799 additions and 0 deletions

View 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

View 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";

View 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";