PHPackages                             condorcet-vote/cef-writer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. condorcet-vote/cef-writer

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

condorcet-vote/cef-writer
=========================

Streaming PHP writer that produces valid Condorcet Election Format (CEF) files line by line.

v1.2.0(1mo ago)136MITPHPPHP ^8.5CI passing

Since May 24Pushed 1mo agoCompare

[ Source](https://github.com/CondorcetVote/CEF-Writer-PHP)[ Packagist](https://packagist.org/packages/condorcet-vote/cef-writer)[ RSS](/packages/condorcet-vote-cef-writer/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (8)Versions (4)Used By (0)

CEF Writer
==========

[](#cef-writer)

A small PHP library that **streams** valid [Condorcet Election Format](https://github.com/CondorcetVote/CondorcetElectionFormat) (CEF) documents to a file or string with a friendly object API.

- Streaming: every `add*()` call writes one line immediately — nothing is buffered, nothing can be edited afterwards.
- Format-safe: the spec's syntactic rules (reserved characters, blank-ballot sentinel, single-line constraints, parameter-before-vote ordering) are enforced. Invalid input throws.
- Semantics-free on purpose: this library checks **format**, never **election logic** (it will, for example, happily let a vote reference a candidate that is not in `#/Candidates:`).
- Works with `\SplFileObject`, `\SplFileInfo`, a filesystem path, or a string passed **by reference**.

**[Full API reference](docs/readme.md)** — generated from the source with [PhpReference](https://github.com/julien-boudry/PhpReference).

Requirements
------------

[](#requirements)

- PHP **8.5** or later
- `ext-mbstring` (used for UTF-8 validation; bundled with PHP)

Installation
------------

[](#installation)

```
composer require condorcet-vote/cef-writer
```

Quick start
-----------

[](#quick-start)

```
use CondorcetVote\CefWriter\Cef;
use CondorcetVote\CefWriter\CommentLine;
use CondorcetVote\CefWriter\VoteLine;
use CondorcetVote\CefWriter\Parameter\CandidatesParameter;
use CondorcetVote\CefWriter\Parameter\ImplicitRankingParameter;
use CondorcetVote\CefWriter\Parameter\WeightAllowedParameter;

$cef = new Cef(file: '/tmp/election.cvotes');

$cef->addComment(new CommentLine('My beautiful election'));
$cef->addParameter(new CandidatesParameter(['Alice', 'Bob', 'Charlie']));
$cef->addParameter(new ImplicitRankingParameter(true));
$cef->addParameter(new WeightAllowedParameter(true));

$cef->addVote(VoteLine::fromRanking(
    ranking: [['Alice'], ['Bob'], ['Charlie']],
    quantifier: 42,
));
$cef->addVote(VoteLine::fromRanking(
    ranking: [['Charlie'], ['Alice', 'Bob']],
    weight: 7,
    quantifier: 8,
));
$cef->addVote(VoteLine::fromRanking(ranking: [])); // blank ballot (/EMPTY_RANKING/)
```

produces:

```
# My beautiful election
#/Candidates: Alice ; Bob ; Charlie
#/Implicit Ranking: true
#/Weight Allowed: true

Alice > Bob > Charlie * 42
Charlie > Alice = Bob ^7 * 8
/EMPTY_RANKING/

```

Output targets
--------------

[](#output-targets)

The `Cef` constructor accepts **exactly one** of the following:

ArgumentTypeBehaviour`file: $path``string`Opened with mode `wb` (created/truncated).`file: $info``\SplFileInfo`Opened with mode `wb`.`file: $object``\SplFileObject` (must already be open)Used as-is.`string: $buf``string` passed **by reference**Each line is appended to the caller's variable.```
$buffer = '';
$cef = new Cef(string: $buffer);
$cef->addParameter(new CandidatesParameter(['A', 'B']));
echo $buffer; // "#/Candidates: A ; B\n"
```

`autoFormat`
------------

[](#autoformat)

`$cef->autoFormat` is a public `bool` (default `true`):

- `true` — writes the **readable** flavor of the spec: spaces around `>`, `=`, `;`, `,`, `||`, `^`, `*`; one blank line is inserted automatically between the parameter block and the first vote.
- `false` — writes the **compact** form with no optional whitespace and no auto blank line.

```
$cef->autoFormat = false;
$cef->addParameter(new CandidatesParameter(['A', 'B']));
$cef->addVote(VoteLine::fromRanking([['A'], ['B']]));
// "#/Candidates:A;B\nA>B\n"
```

Building blocks
---------------

[](#building-blocks)

### Parameters

[](#parameters)

Each standard parameter has its own typed object. Custom parameters are supported via `CustomParameter`. The `StandardParameter` enum lists the exact spec names.

ClassCEF nameConstructor`CandidatesParameter``Candidates``array``NumberOfSeatsParameter``Number of Seats``int >= 1``ImplicitRankingParameter``Implicit Ranking``bool``VotingMethodsParameter``Voting Methods``array``WeightAllowedParameter``Weight Allowed``bool``CustomParameter`(free-form)`string $name, string $value`Parameters can only be added before the first vote — any later call throws `CefFormatException`.

### Vote lines

[](#vote-lines)

`VoteLine` instances are built through static named constructors — its constructor is `@internal`, so never use `new VoteLine(...)`.

The typed way — `VoteLine::fromRanking()` — then pass it to `Cef::addVote()`:

```
VoteLine::fromRanking(
    ranking:       [['Alice'], ['Bob', 'Charlie']], // [] => /EMPTY_RANKING/
    tags:          ['voter@example.com'],
    weight:        7,
    quantifier:    3,
    inlineComment: 'late ballot',
);
```

Each rank is itself a list of tied candidates. An empty top-level ranking emits the `/EMPTY_RANKING/` blank-ballot sentinel.

The `ranking` argument also accepts a ready-made `Ranking` object (see below). Once built, the parsed ranking is exposed as a `Ranking` on the read-only `$voteLine->ranking` property (use `$voteLine->ranking->ranks` for the `list` structure). It is `null` only when the ballot was built from a verbatim string via `VoteLine::fromRawRankingString()` (see below).

#### The `Ranking` value object

[](#the-ranking-value-object)

A ranking can be built, validated and rendered on its own through the `Ranking` class — the same abstraction `VoteLine` uses internally:

```
use CondorcetVote\CefWriter\Ranking;

$ranking = new Ranking([['Alice'], ['Bob', 'Charlie']]); // [] => /EMPTY_RANKING/
$ranking = Ranking::fromString('Alice > Bob = Charlie'); // or parse a ranking-only string

$ranking->ranks;        // [['Alice'], ['Bob', 'Charlie']]
$ranking->format();     // "Alice > Bob = Charlie"   (relaxed flavor)
$ranking->format(false);// "Alice>Bob=Charlie"       (compact flavor)
(string) $ranking;      // same as format()

$cef->addVote(VoteLine::fromRanking(ranking: $ranking, weight: 7));
```

`Ranking` is immutable and self-validating: reserved characters, empty ranks and duplicate candidates throw a `CefFormatException` at construction time. `Ranking::fromString()` accepts **only** a ranking — every reserved character, the `||` tag separator and line breaks are rejected.

#### Verbatim ranking — `VoteLine::fromRawRankingString()`

[](#verbatim-ranking--votelinefromrawrankingstring)

When you already have a ranking as text and want it written **verbatim** (its exact spacing preserved, no re-rendering), build the ballot with `fromRawRankingString()`. It validates the ranking string with the same rules as `Ranking::fromString()` but skips parsing it into a `Ranking` — the string is stored as-is and `$voteLine->ranking` is therefore `null`:

```
$line = VoteLine::fromRawRankingString('Alice>Bob=Charlie', weight: 7);
$line->format(true);  // "Alice>Bob=Charlie ^7"  (ranking kept verbatim)
$line->ranking;       // null
```

Only the library-built companions (the `||` tag separator, `^weight`, `*quantifier`) follow `autoFormat`; the ranking itself is never reformatted. This is the engine behind `Cef::addRawVote()`.

#### From a raw string — `VoteLine::fromString()`

[](#from-a-raw-string--votelinefromstring)

Parse a full CEF vote-line string into a `VoteLine` instance. Every component is optional except the ranking; both the relaxed (`A > B ^7 * 2`) and the compact (`A>B^7*2`) spacing flavors are accepted, plus the `/EMPTY_RANKING/`sentinel.

```
$cef->addVote(VoteLine::fromString('voter@example.com || Alice > Bob ^7 * 3 # late ballot'));
```

The string is parsed, every component is validated against the same rules as the constructor, and a `VoteLine` is returned. Throws `CefFormatException` on any malformed component.

#### Pre-validated raw lines — `Cef::addRawVoteLine()`

[](#pre-validated-raw-lines--cefaddrawvoteline)

When you already have ballots as text and want the fastest write path, `addRawVoteLine()` skips the `VoteLine` allocation while still enforcing the full CEF format:

```
$cef->addRawVoteLine('Alice > Bob = Charlie ^7 * 8');
```

It strips one trailing line terminator (`\r\n`, `\n`, `\r`), trims, rejects empty / multi-line / leading-`#` inputs, then runs `VoteLine::assertValidString()` for the same deep validation as `fromString()`. The `autoFormat` flag is **not** applied — what you pass is what gets written. About 1.8× faster than `addVote(VoteLine::fromString())`in practice.

#### Strict, ranking-only raw votes — `Cef::addRawVote()`

[](#strict-ranking-only-raw-votes--cefaddrawvote)

`addRawVoteLine()` is deliberately permissive: because it accepts a *whole*vote line, the caller can embed tags, a weight, a quantifier or an inline comment directly in the text. When the ranking comes from an untrusted source and you want a hard guarantee that it cannot smuggle any of that in, use the strict sibling `addRawVote()`:

```
$cef->addRawVote('Alice > Bob = Charlie', quantifier: 8, weight: 7, tags: ['voter@example.com']);
// "voter@example.com || Alice > Bob = Charlie ^7 * 8"
```

`$vote` may contain **only** a ranking — candidate names joined by `>` and `=`, or the `/EMPTY_RANKING/` sentinel. Any line break, the `||` tag separator, and every reserved character (`^`, `*`, `#`, `;`, `,`, `/`) is rejected, so the string can never inject a weight, quantifier, tag, inline comment or a second vote. Those companions are supplied **exclusively** through the typed parameters:

```
public function addRawVote(
    string $vote,
    ?int $quantifier = null,
    ?int $weight = null,
    ?array $tags = null,
): self
```

`$weight` and `$quantifier` are nullable and default to `null`, in which case they are omitted from the output (keeping the line as short as possible); when provided they must be strictly positive. Just like `addRawVoteLine()`, the ranking string is written **verbatim** — its original spacing is preserved and `autoFormat` does **not** reformat it. The `autoFormat` flag still governs the layout of the library-built companions (the `||` tag separator, `^weight`, `*quantifier`). Throws `CefFormatException` on any malformed input.

#### Validation-only — `VoteLine::assertValidString()`

[](#validation-only--votelineassertvalidstring)

If you want to validate a vote-line string without allocating a `VoteLine`(e.g. to pre-flight user input before queueing it elsewhere), call the static `assertValidString()` — same pipeline as `fromString()`, no object returned, throws `CefFormatException` on any violation.

### Comments and blank lines

[](#comments-and-blank-lines)

```
$cef->addComment(new CommentLine('section divider'));
$cef->addCommentLine('shortcut — builds the CommentLine for you');
$cef->addEmptyLine();
```

Inline comments attached to vote lines live on `VoteLine::$inlineComment`. The CEF spec forbids inline comments on parameter lines, so the parameter classes intentionally do not expose one.

Errors
------

[](#errors)

Two top-level hierarchies, each for a different layer.

### Format &amp; input violations — `CefFormatException`

[](#format--input-violations--cefformatexception)

Base class for every specification or input violation. Catch this one to handle *any* format-related failure uniformly; catch a specific subclass below to branch on a kind of violation. Each message names the offending field and the rule that was broken.

SubclassThrown for`InvalidUtf8Exception`Byte sequence that does not decode as valid UTF-8.`ReservedCharacterException`One of the spec-reserved characters (`> = ; , # / * ^`), a `:` in a custom parameter name, `||` inside a tag, or a leading `#` on a raw vote line.`InvalidValueException`Empty required string, embedded line break, null byte, non-positive `weight` / `quantifier`, empty `#/Candidates:` or `#/Voting Methods:` list, or empty rank inside a ranking.`DuplicateCandidateException`Same candidate label appearing twice in `#/Candidates:` or anywhere inside a ranking (including across tied groups).`InvalidWriterStateException``Cef` constructed with neither a file nor a string target (or with both); parameter added after the first vote; vote-line string parsed without a ranking.All subclasses extend `CefFormatException`, which itself extends `\RuntimeException`.

### I/O failures — `CefWriteException`

[](#io-failures--cefwriteexception)

Thrown when writing to the underlying target (file or string buffer) fails — typically a closed handle, a read-only file, or a full disk. Distinct from `CefFormatException` because the cause is I/O, not your input. Extends `\RuntimeException`.

Development
-----------

[](#development)

```
composer install
vendor/bin/pest                                # run tests
vendor/bin/phpstan analyse                     # static analysis
vendor/bin/php-cs-fixer fix                    # apply lint
vendor/bin/php-cs-fixer fix --dry-run --diff   # check lint without writing
```

CI (`.github/workflows/ci.yml`) runs the test suite on Linux / Windows / macOS, plus a dedicated job with **JIT function mode** enabled, plus PHPStan and PHP CS Fixer.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance91

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

3

Last Release

43d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4020317?v=4)[Julien Boudry](/maintainers/julien-boudry)[@julien-boudry](https://github.com/julien-boudry)

---

Top Contributors

[![julien-boudry](https://avatars.githubusercontent.com/u/4020317?v=4)](https://github.com/julien-boudry "julien-boudry (28 commits)")

---

Tags

streamingwritervotingelectionCondorcetcef

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/condorcet-vote-cef-writer/health.svg)

```
[![Health](https://phpackages.com/badges/condorcet-vote-cef-writer/health.svg)](https://phpackages.com/packages/condorcet-vote-cef-writer)
```

###  Alternatives

[suin/php-rss-writer

Yet another simple RSS writer library for PHP 5.4 or later.

2831.4M21](/packages/suin-php-rss-writer)[clue/utf8-react

Streaming UTF-8 parser, built on top of ReactPHP.

6611.0M4](/packages/clue-utf8-react)[julien-boudry/condorcet

Complete election manager, providing natively many voting methods including: Condorcet / Borda (+ Nauru variant) / Copeland / Dodgson (2 Approximations) / FTPT / Instant-runoff (alternative vote) / Kemeny–Young / Minimax (+ variants) / Ranked Pairs (+ variants) / Schulze (+ variants), Single Transferable Vote (STV) / Comparison of Pairs of Outcomes by the Single Transferable Vote (CPO-STV)

13110.0k2](/packages/julien-boudry-condorcet)[robtimus/multipart

A library to support creating (streaming) multiparts

141.1M18](/packages/robtimus-multipart)[chiliec/yii2-vote

Provides anonymous voting for any models

716.2k](/packages/chiliec-yii2-vote)[alexandre-daubois/lazy-stream

Write data to streams, only when it is really needed to.

735.9k](/packages/alexandre-daubois-lazy-stream)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
