PHPackages                             jacobjoergensen/laravel-paper - 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. [Database &amp; ORM](/categories/database)
4. /
5. jacobjoergensen/laravel-paper

ActiveLibrary[Database &amp; ORM](/categories/database)

jacobjoergensen/laravel-paper
=============================

A flat-file Eloquent driver for modern Laravel.

1.10.0(1mo ago)925681[1 PRs](https://github.com/JacobJoergensen/laravel-paper/pulls)MITPHPPHP ^8.4CI passing

Since Mar 18Pushed 3d agoCompare

[ Source](https://github.com/JacobJoergensen/laravel-paper)[ Packagist](https://packagist.org/packages/jacobjoergensen/laravel-paper)[ RSS](/packages/jacobjoergensen-laravel-paper/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (18)Versions (16)Used By (0)

Laravel Paper
=============

[](#laravel-paper)

[![Latest Version](https://camo.githubusercontent.com/32f0b580a2651cbd699bcc46df6c097e08cfc9d0c6f9f52f9368bc8c9ab0a254/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a61636f626a6f657267656e73656e2f6c61726176656c2d70617065722e737667)](https://packagist.org/packages/jacobjoergensen/laravel-paper)[![Tests](https://github.com/JacobJoergensen/laravel-paper/actions/workflows/tests.yml/badge.svg)](https://github.com/JacobJoergensen/laravel-paper/actions)[![License](https://camo.githubusercontent.com/c72607a1d02ac48dbd123fcc1fa6ee91940128dc83eb45ec81db86341369e8d2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f4a61636f624a6f657267656e73656e2f6c61726176656c2d7061706572)](LICENSE)

Laravel Paper is a Laravel package that adds flat-file driver support for Eloquent. It supports Markdown and JSON files and works with Laravel 12+ on PHP 8.4+.

Why Laravel Paper?
------------------

[](#why-laravel-paper)

Two PHP 8 attributes and a trait. No custom database connection, no schema, your flat files use Eloquent's familiar query API.

Get Started
-----------

[](#get-started)

```
composer require jacobjoergensen/laravel-paper
```

Defining a Model
----------------

[](#defining-a-model)

Put files in a content directory and point a model at it:

```
use Illuminate\Database\Eloquent\Model;
use JacobJoergensen\LaravelPaper\Attributes\ContentPath;
use JacobJoergensen\LaravelPaper\Attributes\Driver;
use JacobJoergensen\LaravelPaper\Paper;

#[Driver('markdown')]
#[ContentPath('content/posts')]
class Post extends Model
{
    use Paper;
}
```

The filename without extension becomes the slug, which is the primary key.

Markdown Example
----------------

[](#markdown-example)

A post:

```
---
title: Building a Blog with Flat Files
published: true
date: 2024-03-15
tags: [laravel, markdown]
---

Your Markdown content goes here...
```

> YAML reads an unquoted `date: 2024-03-15` as a Unix timestamp. Quote it or cast it with `'date' => 'date'` so comparisons like `where('date', '>', '2024-01-01')` work.

Query it like any other Eloquent model:

```
// Get all published posts
$posts = Post::where('published', true)
    ->orderBy('date', 'desc')
    ->get();

// Find by slug
$post = Post::where('slug', 'flat-file-blog')->first();

// Filter by tag (whereContains checks membership of an array field)
$laravelPosts = Post::whereContains('tags', 'laravel')->get();

// Match a substring in a string field
$intro = Post::whereLike('title', '%hello%')->get();

// Search a value across multiple columns
$results = Post::whereAny(['title', 'content'], 'like', '%flat-file%')->get();
```

Use it in your views:

```
@foreach($posts as $post)

        {{ $post->title }}
        {{ $post->date }}
        {!! Str::markdown($post->content) !!}

@endforeach
```

JSON Files
----------

[](#json-files)

Works the same way with JSON:

```
{
    "name": "Jacob Jørgensen",
    "role": "Developer",
    "github": "jacobjoergensen"
}
```

```
#[Driver('json')]
#[ContentPath('content/team')]
class TeamMember extends Model
{
    use Paper;
}
```

```
$team = TeamMember::all();
$devs = TeamMember::where('role', 'Developer')->get();
```

File Naming and Slugs
---------------------

[](#file-naming-and-slugs)

The filename (without extension) is the slug:

```
content/posts/
├── hello-world.md        → slug: "hello-world"
├── my-second-post.md     → slug: "my-second-post"
└── draft-post.md         → slug: "draft-post"

```

```
$post = Post::find('hello-world');
$posts = Post::findMany(['hello-world', 'my-second-post']);
```

To change a slug, rename the file. For a URL that differs from the filename, add a frontmatter field and route on that instead:

```
---
title: Hello World
permalink: /blog/2024/hello-world
---
```

Writing
-------

[](#writing)

Paper models save and delete files using the standard Eloquent API:

```
$post = new Post();
$post->slug = 'hello-world';
$post->title = 'Hello World';
$post->content = 'My first post.';
$post->save();

$post->title = 'Updated title';
$post->save();

$post->delete();
```

Save and delete fire the usual model events, and loading a record fires `retrieved`.

For attribute-array creation:

```
Post::create([
    'slug' => 'hello-world',
    'title' => 'Hello World',
]);

Post::firstOrCreate(
    ['slug' => 'hello-world'],
    ['title' => 'Hello World'],
);

Post::updateOrCreate(
    ['slug' => 'hello-world'],
    ['title' => 'Updated title'],
);
```

`create` requires a `slug` and does not derive one from other fields. If your source data has no slug, generate one yourself with `Str::slug($title)`.

For bulk edits, `update` sets values across every matching record:

```
Post::where('draft', true)->update(['published' => true]);
```

It writes each matching file in a loop, so model events fire per record and `$fillable` does not apply. It is not a single atomic operation.

To save or delete without firing events:

```
$post->saveQuietly();
$post->deleteQuietly();
```

To reload from disk, `fresh()` returns a new instance and `refresh()` updates the current one in place:

```
$fresh = $post->fresh();
$post->refresh();
```

Timestamps
----------

[](#timestamps)

Paper models have no timestamps by default. Add `#[Timestamps]` to expose the file's modification time as `updated_at`:

```
use JacobJoergensen\LaravelPaper\Attributes\Timestamps;

#[Driver('markdown')]
#[ContentPath('content/posts')]
#[Timestamps]
class Post extends Model
{
    use Paper;
}
```

```
$post = Post::find('hello-world');
$post->updated_at;                          // Carbon instance from the file's mtime

$recent = Post::latest('updated_at')->get();
```

`updated_at` comes from the file's mtime and is never written to frontmatter. `created_at` isn't derived; set it in frontmatter if you need it. A Git checkout resets mtimes to the deploy time, so use this for content edited in place and keep a frontmatter `date` for Git-deployed content.

Pagination
----------

[](#pagination)

```
$posts = Post::paginate(15);
$posts = Post::simplePaginate(15);
```

Use `simplePaginate` for large directories where the count is expensive, and you don't need a total.

Aggregates
----------

[](#aggregates)

Alongside `count`, Paper has `min`, `max`, `sum`, `avg`, and its alias `average`:

```
$next = Post::max('order') + 1;
$views = Post::where('published', true)->sum('views');
```

On an empty result `sum` returns `0` and the rest return `null`. Null, missing, and non-numeric values are ignored, the same way SQL aggregates skip `NULL`.

Relationships
-------------

[](#relationships)

For relationships, use `belongsToPaper` and `hasManyPaper`:

```
class Post extends Model
{
    use Paper;

    public function author()
    {
        return $this->belongsToPaper(Author::class);
    }
}

class Author extends Model
{
    use Paper;

    public function posts()
    {
        return $this->hasManyPaper(Post::class);
    }
}
```

```
$post = Post::find('hello-world');
$author = $post->author();

$author = Author::find('jane-doe');
$posts = $author->posts();
```

Call these as methods, not properties. Foreign keys default to `{model}_slug` (e.g. `author_slug`). Pass a second argument to override.

Validation
----------

[](#validation)

Use `PaperRule` with Laravel's validator:

```
use JacobJoergensen\LaravelPaper\Rules\PaperRule;

$request->validate([
    'slug' => ['required', PaperRule::unique(Post::class)],
    'author_slug' => ['required', PaperRule::exists(Author::class)],
]);
```

To skip the current record on update:

```
PaperRule::unique(Post::class)->ignore($post->slug);
```

Custom Drivers
--------------

[](#custom-drivers)

Markdown and JSON ship by default. To support another format, implement `DriverContract` and register it in a service provider:

```
use JacobJoergensen\LaravelPaper\Contracts\DriverContract;
use JacobJoergensen\LaravelPaper\Drivers\DriverRegistry;

final class YamlDriver implements DriverContract
{
    public function extensions(): array
    {
        return ['yaml', 'yml'];
    }

    public function parse(string $filepath): array
    {
        // return the file's data as an array
    }

    public function serialize(array $data): string
    {
        // return the file contents to write
    }
}
```

```
public function boot(): void
{
    app(DriverRegistry::class)->register('yaml', YamlDriver::class);
}
```

Then point a model at it with `#[Driver('yaml')]`.

AI-Assisted Development
-----------------------

[](#ai-assisted-development)

Paper ships a [Laravel Boost](https://laravel.com/docs/boost) skill. If your project uses Boost, `php artisan boost:install` offers to install it, giving your AI agent Paper-specific guidance for writing and querying flat-file models.

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md) for filing bugs and submitting PRs.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE.md).

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance96

Actively maintained with recent releases

Popularity31

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.1% 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 ~6 days

Recently: every ~12 days

Total

12

Last Release

36d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ec76e17c5e9dc5b5d68e03cec18ba374c7aeb34bc566da4764ed125ba8bf8453?d=identicon)[JacobJoergensen](/maintainers/JacobJoergensen)

---

Top Contributors

[![JacobJoergensen](https://avatars.githubusercontent.com/u/96596794?v=4)](https://github.com/JacobJoergensen "JacobJoergensen (109 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laraveleloquentmarkdowncontentflat file

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jacobjoergensen-laravel-paper/health.svg)

```
[![Health](https://phpackages.com/badges/jacobjoergensen-laravel-paper/health.svg)](https://phpackages.com/packages/jacobjoergensen-laravel-paper)
```

###  Alternatives

[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[laravel/ai

The official AI SDK for Laravel.

9782.1M162](/packages/laravel-ai)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[watson/validating

Eloquent model validating trait.

9733.4M53](/packages/watson-validating)[plank/laravel-mediable

A package for easily uploading and attaching media files to models with Laravel

8251.6M13](/packages/plank-laravel-mediable)[cybercog/laravel-love

Make Laravel Eloquent models reactable with any type of emotions in a minutes!

1.2k322.4k1](/packages/cybercog-laravel-love)

PHPackages © 2026

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