Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Set Type Bundle Laravel Package

raksul/doctrine-set-type-bundle

A Symfony bundle that provides a Doctrine DBAL type for MySQL SET columns, enabling entities to map set-valued database fields to PHP arrays and simplifying Doctrine ORM integration for applications that rely on MySQL SET types.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle is a thin wrapper that registers a custom Doctrine DBAL type (set) so Doctrine can map MySQL SET columns to PHP arrays.

Installation

composer require raksul/doctrine-set-type-bundle

Register the bundle in config/bundles.php (Symfony Flex may do this for you):

return [
    // ...
    Raksul\DoctrineSetTypeBundle\DoctrineSetTypeBundle::class => ['all' => true],
];

Where to look first

  1. The bundle's DoctrineSetType (or similarly named) class under src/ — this is the only place real logic lives, and it tells you exactly which column options key the type reads for the allowed values.
  2. The DependencyInjection/ compiler pass that registers the type with Doctrine — confirms the type name (usually set).
  3. Any README examples in the repo — they show the expected options shape for the column definition.

First use case

Add a SET column to an entity and map it to an array:

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Article
{
    #[ORM\Column(
        type: 'set',
        options: ['values' => ['draft', 'review', 'published', 'archived']]
    )]
    private array $statuses = [];
}

Then run bin/console doctrine:schema:validate and doctrine:migrations:diff to confirm a SET('draft','review','published','archived') column is emitted. Read/write through the property and you get a PHP array; the bundle handles the a,b,c string conversion.

Implementation Patterns

Attribute mapping (preferred in modern Symfony)

#[ORM\Column(
    type: 'set',
    nullable: true,
    options: ['values' => ['tag_a', 'tag_b', 'tag_c']]
)]
private ?array $tags = null;

Use array (not ?array) with a default of [] if you want the empty set ('' in MySQL) rather than NULL to be the "no selections" state — the two are semantically different in MySQL.

Form binding

The type round-trips as a PHP array, so bind directly to ChoiceType with multiple: true:

$builder->add('tags', ChoiceType::class, [
    'choices' => ['Tag A' => 'tag_a', 'Tag B' => 'tag_b', 'Tag C' => 'tag_c'],
    'multiple' => true,
    'expanded' => true,
]);

Validate the selection against the DB definition (a Choice/All constraint or a #[Assert\Choice]) — the bundle will happily persist strings MySQL rejects only after the column definition is set, so keep the two in sync.

Querying SET columns

Doctrine's DQL has no native FIND_IN_SET. Two practical options:

  • Native SQL for set-membership queries:
$sql = 'SELECT * FROM article WHERE FIND_IN_SET(:tag, tags) > 0';
$rows = $em->getConnection()->fetchAllAssociative($sql, ['tag' => 'tag_a']);
  • Keep the set column for storage and denormalize the "hot" filter into a separate boolean or join table if you query it often — FIND_IN_SET cannot use an index.

Ordering by set values

MySQL's SET is a bitmask; ordering by the column sorts by bit position, which matches the order in options.values, not alphabetically. This is often what you want (draft < review < published) but is worth being explicit about in code comments.

Migrations

The diff command generally emits the full SET(...) declaration from the options array, but treat generated migrations as a draft:

  • Adding/removing values from options.values should be reviewed in the generated ALTER TABLE ... MODIFY.
  • Test on MariaDB as well as MySQL; the syntax is compatible but behavior on trailing values can differ.

If diff doesn't pick up a changed value list, hand-write the MODIFY COLUMN in the migration — the metadata-only change doesn't always trigger a diff.

Repositories / lifecycle

Because convertToPHPValue returns an array, remember:

public function addStatus(string $status): self
{
    if (!in_array($status, $this->statuses, true)) {
        $this->statuses[] = $status;
    }
    return $this;
}

Doctrine tracks changes to the array property by reference comparison, so mutate via a new array (or explicitly clone) when in doubt; in practice reassignment ($this->statuses = [...$this->statuses, $status]) is the safest pattern.

Gotchas and Tips

MySQL/MariaDB only. If the same entity is ever run against PostgreSQL or SQLite (e.g., test suites), the type will fail. Use a compatible type or a platform-conditional mapping; the bundle does not abstract this.

Hard limit of 64 values in a MySQL SET. Plan for a join table if the allowed values are user-tunable or could grow.

Empty set vs NULL. MySQL stores the empty set as '', not SQL NULL. convertToDatabaseValue typically converts [] → '' and null → null. Mixing both as "empty" leads to subtle bugs in IS NULL / = '' filters.

Whitespace and commas in values. A value containing a comma will corrupt the stored representation on round-trip. Never put a , inside an allowed value; validate this at configuration time. Same for leading/trailing whitespace.

Order matters. The value order in options.values defines the bitmask. Reordering values later silently changes numeric/ordering comparisons and any bitmask usage. To change order, add the new value to the end and remove the old one.

Type name collision. The bundle registers a type named set. If another package (or your own code) also registers set, the last registration wins. Grep config/packages/doctrine.yaml under dbal.types for overrides.

Compiler pass ordering. If you need to customize the type (subclass it, add a custom platform), do it in a compiler pass with a higher priority than the bundle's pass, or override the service definition id rather than the type name.

Schema validation noise. doctrine:schema:validate sometimes complains about the column comment/collation on SET columns. If the diff keeps proposing the same MODIFY, it's usually a collation mismatch — set an explicit collation in options (or the field declaration) so both sides agree.

Debugging what Doctrine actually sends. When the round-trip misbehaves, log the raw value in a listener on postLoad / prePersist, or inspect the SQL with doctrine.dbal.logging. Nine times out of ten the issue is an options.values mismatch between the attribute and the actual DB column.

Extension points. The type itself is the natural extension point: subclass it to add validation (reject values not in options.values before hitting the DB) or to normalize the array. Register your subclass under the same set name in doctrine.dbal.types to have it used everywhere.

Maintenance signal. Only one release (2023) and no dependents — pin to the current version and be prepared to fork if you need changes for a newer Doctrine DBAL version; the code is small enough to maintain in-house if necessary.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums