November 14, 2024 - 11 min

Introduction to AI-Driven PHP Development: Automating Entities with Symfony and OpenAI


				
				

Anka Bajurin Stiskalov

Engineering Lead

featured image for the blog post titled: Automating Entities with Symfony and OpenAI

Learn how to automate PHP entity creation using Symfony components and OpenAI, reducing manual coding and streamlining development for your next project.


In this blog post, we’ll explore how to automate PHP entity creation using Symfony components and OpenAI’s GPT-4. If you’ve ever had to manually define entity classes, manage field definitions, or handle relationships in Symfony applications, you know how repetitive and error-prone the process can become. By leveraging the power of AI, we can streamline this workflow, automating much of the entity generation based on simple user input.


This post covers how I built a CLI-based tool that asks users for entity specifications and then generates complete PHP entities and repositories, all with the help of OpenAI’s language model. We’ll dive into the Symfony components that make this possible, explain how OpenAI fits into the development process, and show you how to integrate AI into your own PHP projects. Whether you’re building new applications or enhancing existing ones, this approach can save you time and reduce coding fatigue.


Introduction


To learn more about creating your own console and using Symfony components to build CLI-based tools, check out our previous blog posts:



In this continuation of building powerful CLI tools using Symfony components, we move into a more sophisticated example—automating the generation of PHP entities with the help of AI, specifically OpenAI’s GPT-4. This approach streamlines the process of defining your data models and relationships, enhancing development speed, and minimizing human errors when defining repetitive structures in your codebase.


We’ll explore how I built an entity generator that takes user input, processes it through OpenAI, and returns complete PHP entities with relationships and repositories—ready to use in a Symfony project.


The Objective: Automating Entity Creation


Manually creating entities can be tedious, especially when working with multiple related entities and their associated fields. Symfony’s Maker Bundle offers some relief by automating parts of this process, helping developers create entities, commands, controllers, form classes, and more. However, even with the Maker Bundle, complex projects often require manual tweaks to ensure that each entity fully integrates with project requirements, such as custom traits, serializer groups, or specific ApiPlatform endpoints.


While the Maker Bundle is helpful for basic cases, the process quickly becomes time-consuming as complexity grows. Extending it with custom commands can help, but it still involves repetitive work and leaves room for human error.


Why Automate with AI vs. Using the Maker Bundle?


Using AI to automate entity generation is a game-changer in terms of speed and simplicity compared to Symfony’s Maker bundle. With the Maker bundle, creating an entity involves running multiple commands (make:entity for each entity) and then manually defining fields, types, and relationships. Furthermore, it requires additional adjustments to meet specific requirements like adding serializer groups, custom traits, ApiPlatform endpoints, or specific relationships. This becomes time-consuming when dealing with multiple entities that have complex relationships.


In contrast, our AI-powered CLI tool allows you to describe your requirements in a single, natural language prompt. For example, a prompt like:


“App requires Restaurant with name, address, openingDate, rating, and one-to-many relationship with Employer; Employer with name, surname, role, one-to-many relation to Employee and many-to-one relation to Restaurant; Employee with one-to-one relation to User, many-to-one relation to Employer, name, surname, age, role”


Using OpenAI, this request is processed in seconds to generate a well-structured JSON specification, which is then automatically converted into complete PHP entities with fields, types, relationships, and required annotations.


This is the example of the specification file and it’s structure:


{
"entities": {
"Videostore": {
"fields": {
"id": {
"type": "int",
"primary_key": true,
"auto_increment": true
},
"name": {
"type": "string",
"nullable": false,
"length": 255
},
"location": {
"type": "string",
"nullable": false,
"length": 255
},
"workingHours": {
"type": "string",
"nullable": false,
"length": 255
}
},
"relationships": {
"dvds": {
"type": "one-to-many",
"target_entity": "Dvd",
"mapped_by": "videostore"
},
"customers": {
"type": "one-to-many",
"target_entity": "Customer",
"mapped_by": "videostore"
}
}
},
"Dvd": {
"fields": {
"id": {
"type": "int",
"primary_key": true,
"auto_increment": true
},
"title": {
"type": "string",
"nullable": false,
"length": 255
},
"year": {
"type": "int",
"nullable": false
},
"actors": {
"type": "string",
"nullable": false,
"length": 255
},
"duration": {
"type": "int",
"nullable": false
},
"genre": {
"type": "string",
"nullable": false,
"length": 255
}
},
"relationships": {
"videostore": {
"type": "many-to-one",
"target_entity": "Videostore",
"inversed_by": "dvds",
"foreign_key": "videostore_id"
}
}
},
"Customer": {
"fields": {
"id": {
"type": "int",
"primary_key": true,
"auto_increment": true
},
"name": {
"type": "string",
"nullable": false,
"length": 50
},
"surname": {
"type": "string",
"nullable": false,
"length": 50
}
},
"relationships": {
"videostore": {
"type": "many-to-one",
"target_entity": "Videostore",
"inversed_by": "customers",
"foreign_key": "videostore_id"
},
"user": {
"type": "one-to-one",
"target_entity": "User",
"foreign_key": "user_id"
}
}
}
}
}

How Much Faster Is It?


Based on common scenarios, using OpenAI for this process can be up to 5-10 times faster than using the Maker bundle, depending on the complexity of the entities:


1. Single Command vs. Multiple Steps: With the Maker bundle, you need to define each entity and relationship step-by-step, sometimes requiring 5-10 separate commands and additional manual adjustments for traits, serializer groups, and other elements. With OpenAI, a single instruction covers all entities and relationships.


2. No Manual Adjustments: Each entity generated by OpenAI is tailored to fit our Symfony skeleton from the start, with traits, relationships, and ApiPlatform readiness. In contrast, using the Maker bundle typically requires manual code adjustments afterward, which can add significant time.


Our team has fine-tuned a custom Symfony skeleton over the years to support high standards in application architecture, including design patterns, authentication, testing, and more. This means each generated entity must have a specific structure that aligns with our standards. For example, here’s a typical entity structure:


<?php


declare(strict_types=1);


namespace App\Entity;


use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Core\Constant\Constants;
use Core\Entity\EntityInterface;
use Core\Traits\CreatedByTrait;
use Core\Traits\TimestampableTrait;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use App\Repository\EmployeeRepository;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;


#[
ORM\Table(name: 'employee'),
ORM\Entity(repositoryClass: EmployeeRepository::class),
ORM\HasLifecycleCallbacks()
]
#[ApiResource(
operations: [
new Post(
uriTemplate: '/employees',
normalizationContext: [
'groups' => ['Employee:get'],
],
denormalizationContext: [
'groups' => ['Employee:create'],
],
name: 'api_v1_employees_create',
),
new GetCollection(
uriTemplate: '/employees',
normalizationContext: [
'groups' => ['Employee:get', 'item:created_by'],
],
name: 'api_v1_employees_index',
),
new Get(
uriTemplate: '/employees/{id}',
normalizationContext: [
'groups' => ['Employee:get', 'Employee:users', 'user:get'],
],
name: 'api_v1_employees_get',
),
new Put(
uriTemplate: '/employees/{id}',
normalizationContext: [
'groups' => ['Employee:get'],
],
denormalizationContext: [
'groups' => ['Employee:update'],
],
name: 'api_v1_employees_update_full',
),
new Patch(
uriTemplate: '/employees/{id}',
normalizationContext: [
'groups' => ['Employee:get'],
],
denormalizationContext: [
'groups' => ['Employee:update'],
],
name: 'api_v1_employees_update',
),
new Delete(
uriTemplate: '/employees/{id}',
name: 'api_v1_employees_delete'
),
],
routePrefix: '/' . Constants::API_VERSION_V1,
mercure: false,
)]
class Employee implements EntityInterface, \Stringable
{
use TimestampableTrait;
use CreatedByTrait;


#[
ORM\Column(name: 'id', type: Types::INTEGER),
ORM\GeneratedValue(strategy: 'AUTO'),
ORM\Id,
Assert\Type(Types::INTEGER, groups: ['Employee:create', 'Employee:update']),
Groups(['Employee:get', 'Employee:id'])
]
private ?int $id = null;

3. Simplicity and Ease of Use: Using OpenAI also makes the process far easier by letting you describe the entities in natural language rather than having to remember and type specific commands and fields repeatedly. This allows you to focus on high-level logic rather than low-level implementation details. This approach not only reduces coding fatigue but also minimizes the risk of mistakes, as the AI consistently applies the structure and patterns defined in the prompt.


How the Automation Works: Step-by-Step


Below is an outline of the key steps in the automated process, which demonstrate how OpenAI and Symfony components work together to streamline entity creation.


Step 1: Requesting Entity Configuration via CLI


We begin with the CreateNewAppCommand.php, which prompts the user for inputs like the application name and directory path. This command also manages the folder structure by creating necessary directories (e.g., the app folder and subfolders). To handle folder creation, we use the Symfony Filesystem component, which simplifies managing directories and files.


use Symfony\Component\Filesystem\Filesystem;

$filesystem = new Filesystem();
$filesystem->mkdir($appDirectory);

Once the folder structure is set up, CreateNewAppCommand.php triggers the GitCloneCommand.php to git clone our Symfony skeleton repository into the provided directory path. The cloned repository is placed inside the app folder created based on the user’s input. This ensures that the project setup follows our internal standards.


We use the Symfony Process component in GitCloneCommand.php to automate the cloning process:


use Symfony\Component\Process\Process;

$process = new Process(['git', 'clone', 'repository-url', $appDirectory]);
$process->run();

This seamless flow — from requesting user input, creating the app folder, and cloning the Symfony skeleton—ensures the project is initialized correctly and ready for further configuration.


Step 2: Generating the JSON Configuration


Next CreateNewAppCommand.php command uses a separate command (GenerateConfigJsonCommand.php) to generate the required JSON configuration for our entities.



We’ve built a custom service, OpenAiService, that interacts with OpenAI’s API to handle the request and return the generated code. Here’s a breakdown of how this works:


1) Interacting with OpenAI: When the The GenerateConfigJsonCommand.php processes the user-provided instructions, it sends the instructions to OpenAI through the OpenAiService. This service interacts with OpenAI’s GPT-4 to generate the desired structure in JSON.


// Example of interaction with OpenAI
$response = $this->client->chat()->create([
'model' => 'gpt-4',
'messages' => [
['role' => 'system', 'content' => $systemMessage],
['role' => 'user', 'content' => $instruction]
],
]);

2) Handling User Input: The user-provided input is passed as a prompt to OpenAI, which then generates structured JSON code based on this input.


{
$io = new SymfonyStyle($input, $output);
$helper = $this->getHelper('question');


// Ask for instructions for the structured file
$userInstruction = $io->ask('Please provide instructions for the config JSON file
(e.g., App requires Dealer with name, surname, city, etc.).');

Here’s the prompt I used to generate the correct structure for the JSON specification file:


Predefined system and user messages for JSON generation
$systemMessage = 'You are a helpful assistant. You must return only valid JSON.';
$instruction = 'Please generate a valid JSON representation of Symfony entities
based on the following instructions: ' . $userInstruction . '.
1. Exclude the "User" entity if it is provided in instructions since it already exists.
Make relations to "User" as if it is already defined in the system.
2. For each entity, return a JSON object with the following structure:
{
"entities": {
"EntityName": {
"fields": {
"fieldName": {
"type": "string | int | datetime | other",
"nullable": true | false,
"length": 50 | 255 | other, // if applicable
"primary_key": true | false, // if applicable
"auto_increment": true | false // if applicable
}
},
"relationships": {
"relationName": {
"type": "one-to-many | many-to-one | one-to-one",
"target_entity": "TargetEntity",
"mapped_by": "FieldName", // if applicable
"inversed_by": "FieldName" // if applicable
}
}
}
}
}
3. For fields:
- Use "int" for ID fields, make them auto-incrementing, and set them as primary keys.
- Use "string" with length 255 for most fields unless otherwise specified.
- Use "datetime" for date fields.
- Assume fields are non-nullable unless specified otherwise.
- Do **not** add separate fields for foreign keys (like `employer_id` or `user_id`) if
a relationship is already defined under relationships.
4. For relationships:
- For one-to-many or many-to-one, ensure you define both the foreign key and the relationship
in the JSON.
- Use foreign key fields with type "int".
5. For string fields like name or surname, assume a length of 50 unless otherwise specified.
6. Ensure the output is a valid JSON object, using the top-level key "entities" with entity names
as nested keys.


Here is an example output structure for an "Employer" entity:
{
"entities": {
"Employer": {
"fields": {
"id": {
"type": "int",
"primary_key": true,
"auto_increment": true
},
"name": {
"type": "string",
"nullable": false,
"length": 50
},
"dob": {
"type": "datetime",
"nullable": false
},
"role": {
"type": "string",
"nullable": false,
"length": 255
}
},
"relationships": {
"employees": {
"type": "one-to-many",
"target_entity": "Employee",
"mapped_by": "employer"
}
}
},
"Employee": {
"fields": {
"id": {
"type": "int",
"primary_key": true,
"auto_increment": true
},
"name": {
"type": "string",
"nullable": false,
"length": 50
},
"dob": {
"type": "datetime",
"nullable": false
},
"role": {
"type": "string",
"nullable": false,
"length": 255
}
},
"relationships": {
"employer": {
"type": "many-to-one",
"target_entity": "Employer",
"inversed_by": "employees",
"foreign_key": "employer_id"
}
}
}
}
}';

This request prompts OpenAI to generate a well-structured JSON file that defines all entities and their relationships. Automating this process simplifies managing large specification files, ensuring consistency and reducing human error. OpenAI plays a crucial role here, as it helps ensure that the generated structure aligns with the specifications provided.


The system message allows us to specify the “persona” of the model, instructing it to behave in a certain way. In this case, I used $systemMessage = ‘You are a helpful assistant. You must return only valid JSON.’ to guide the model’s behavior and focus its output on providing strictly valid JSON without extra notes or explanations.


Achieving consistent, high-quality results requires a precise and well-defined prompt. The accuracy of the generated structure heavily depends on the clarity of this prompt, so refining and testing it is essential.


For more on prompt engineering, including techniques for crafting effective prompts, you can visit OpenAI’s Prompt Engineering Guide.


It’s also important to note that OpenAI sometimes returns additional notes or text alongside the code. Therefore, it’s a good practice to validate the generated output to ensure it’s a valid JSON structure.


 // Extract the content (assumed to be valid structure in the requested format)
$generatedContent = trim($response['choices'][0]['message']['content']);
// Validate based on the requested type
if ($type === 'JSON' && !$this->isJson($generatedContent)) {
return null; // Return null if it's not valid JSON
}

We use Symfony Filesystem to store the generated specification file in the app’s folder.


Step 3: Leveraging OpenAI for PHP Entity Generation


Now the one of the most exciting parts of this system is the ability to generate PHP code automatically by integrating OpenAI. By passing specific instructions through OpenAI’s GPT-4, we can generate the PHP entities and repositories based on a JSON configuration, which drastically reduces the amount of manual coding.



To avoid hitting OpenAI’s rate limits, a sleep(30); delay is added within the loop that generates each entity. This pause ensures each request has enough time to complete without exceeding the maximum allowed requests or tokens per minute. By spacing out the API calls, the tool avoids interruptions, allowing entity generation to run smoothly and without triggering rate restrictions.


Crafting Effective Prompts for Consistent Results: A critical part of working with OpenAI in your project is crafting prompts that yield exactly the kind of code you need. This part is tricky because the language model can generate unexpected results if the instructions aren’t clear or detailed enough. In our case, generating accurate PHP entities with Symfony annotations required a prompt that was both precise and exhaustive.


Here’s an shortened example of the prompt I used to generate an entity class through OpenAI:


 


Generate the entity PHP class using OpenAiService
$systemMessage = 'You are a helpful assistant. You must return only valid PHP code for an entity
class.';
$userMessage = <<<TEXT
Please generate a valid PHP entity class based on the following instructions:
$instruction


1. The output should be **strictly PHP code** with no comments, notes, or additional explanations at the end.
. . .
8. Ensure the class implements necessary Symfony traits and interfaces (e.g., \Stringable, EntityInterface).
. . .
12. Ensure that all getters and setters are present and correct. For instance, the `getId()`
method should return `\$this->id`, not `\$id`.
13. Implement the `__toString()` method.
. . .
15. Use this template as an example:
$exampleEntityClass
16. Ensure that new entities should have namespace `App\Entity`.
. . .
20. Ensure that bi-directional relationships are symmetrical:
- For bi-directional relationships, always include both `mappedBy` on the **inverse side**
and `inversedBy` on the **owning side**.
- Verify that the fields reference each other correctly (e.g., `OneToMany` on one side,
and `ManyToOne` on the other side).
21. Validate that all relationships from the configuration are implemented in the generated entity.


TEXT;

Cleaning the Output: Once OpenAI returns the generated PHP code, we clean it to ensure it is properly formatted according to our Symfony project’s requirements. The OpenAiService includes methods to remove unnecessary text and ensure the content is only PHP code.


private function cleanGeneratedPhpCode(string $generatedContent): string
{
$generatedContent = str_replace('```php', '', $generatedContent);
$generatedContent = str_replace('```', '', $generatedContent);

// Strip anything after "Note" or other non-code markers
$cleanedContent = preg_replace('/Note.*$/s', '', $generatedContent);

return trim($cleanedContent);
}

Storing the Output: Once OpenAI returns the generated PHP code, we use Symfony Filesystem again to store the generated entity classes. The Filesystem component allows us to write the generated code directly into the correct directories making the entities immediately ready for use:


$filesystem->dumpFile($filePath, $generatedCode);

Through this automated process, we eliminate much of the repetitive work of manually creating and configuring entities. Instead, OpenAI takes the user instructions, generates code based on predefined patterns, and the service cleans the output to ensure its compatibility with the rest of the application.


Step 4: Automating Repositories


For each entity generated, the corresponding repository class is also created. For this we don’t need OpenAI since the template is simple and we can just replace placeholders and store the file with Symfony Filesystem.


private function generateRepositoryClass(string $entityName, string $outputDirectory): void
{
$repositoryClassContent = <<<PHP
<?php


declare(strict_types=1);


namespace App\Repository;


use Core\Repository\BaseRepository;
use App\Entity\\$entityName;


final class {$entityName}Repository extends BaseRepository
{
public const ENTITY_CLASS_NAME = $entityName::class;
}


PHP;


$repositoryDirectory = $outputDirectory . '/Repository';


// Ensure the repository directory exists
if (!file_exists($repositoryDirectory)) {
mkdir($repositoryDirectory, 0777, true);
}


// Save the repository class
$repositoryFilePath = $repositoryDirectory . "/{$entityName}Repository.php";
file_put_contents($repositoryFilePath, $repositoryClassContent);
}

By automating both entities and repositories, this tool offers a significant productivity boost in application development.


Conclusion


This AI-driven approach to PHP development offers an exciting glimpse into the future of automated coding. By leveraging OpenAI with Symfony components, we can drastically reduce the amount of boilerplate code, allowing developers to focus on business logic and other high-level tasks. In the next part of this series, we’ll explore how this system can be expanded to handle more advanced use cases and dive deeper into its architecture.




Summary of Symfony Components Used:



  • Symfony Console: Handles user input and CLI command execution.

  • Symfony Process: Automates external commands like git clone.

  • Symfony Filesystem: Manages directories and stores generated entity classes.

  • OpenAI Integration: Generates entity classes and repositories based on user prompts.


Give Kudos by sharing the post!

Share:

ABOUT AUTHOR

Anka Bajurin Stiskalov

Engineering Lead

Engineering Lead at Q. Passionate about developing with a focus on making life easier for fellow developers. Always looking for ways to streamline workflows and boost productivity — let's build smarter together!