PHPackages                             railroad/railcontent - 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. [Admin Panels](/categories/admin)
4. /
5. railroad/railcontent

ActiveLibrary[Admin Panels](/categories/admin)

railroad/railcontent
====================

Very simple and flexible CMS system for developers.

v3.0.29(11mo ago)0103.5k2[2 PRs](https://github.com/railroadmedia/railcontent/pulls)2MITPHPPHP ^8.2

Since Sep 12Pushed 7mo ago2 watchersCompare

[ Source](https://github.com/railroadmedia/railcontent)[ Packagist](https://packagist.org/packages/railroad/railcontent)[ RSS](/packages/railroad-railcontent/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (18)Versions (648)Used By (2)

Railcontent
===========

[](#railcontent)

Data first simple CMS.

- [Railcontent](#railcontent)
    - [Progress-Bubbling](#progress-bubbling)
        - [Example](#example)
    - [Validation](#validation)
        - [Non-technical Introduction](#non-technical-introduction)
        - [Technical Introduction](#technical-introduction)
        - [About Defining Rules](#about-defining-rules)
        - [Note about field or datum that reference another piece of content](#note-about-field-or-datum-that-reference-another-piece-of-content)
        - [Important Note about the "numeric" rule](#important-note-about-the--numeric--rule)
        - [Specifying rules](#specifying-rules)
        - [Details of options available for each brand](#details-of-options-available-for-each-brand)
            - [fields](#fields)
            - [data](#data)
            - [number\_of\_children](#number-of-children)
        - [Configuration Example](#configuration-example)
        - [MultipleColumnExistsValidator](#multiplecolumnexistsvalidator)
            - [WHERE *AND*](#where--and-)
            - [WHERE *OR*](#where--or-)

Progress-Bubbling
-----------------

[](#progress-bubbling)

When a content has its progress saved, a `UserContentProgressSaved` event is fired. This event triggers the `bubbleProgress` method of `UserContentProgressService`. If the content saved—that triggered the event—has a parent, the type of that parent will be evaluated against config values defined in you're application's copy of */config/railcontent.php*.

This is useful when you have a kind of content (like "course" for Drumeo) that should be marked as started when a child is started. However, if you have a kind of content that you don't want marked as started (say perhaps because it has it's own system, like Learning-Paths for Drumeo), you don't want the parent started when the child is. Keep in mind that children can have multiple parents, thus if a lesson is started we may want that to cause one parent to be started, but another to *remain unstarted*.

You can restrict which content **parent** types should be marked as started or completed when their children are started by ***omitting*** them from the allowed\_types lists.

```
'allowed_types_for_bubble_progress' => [
    'started' =>    [
        'foo','bar', 'baz'
    ],
    'completed' =>  [
        'foo','bar', 'qux'
    ]
];
```

You can have a value in both list if it has no restrictions, or just one, or neither.

Note that this only applies for progress ***bubbling***. This does not effect the `startContent`, `completeContent`, or `saveContentProgress` methods of `UserContentProgressService`.

### Example

[](#example)

Say you have a parent of a type for which *bubbling of **started** progress event is **not** allowed*.

If that parent is itself **not yet started**, it will not have progress record written or updated when one of it's children is. If it's in the "completed" allowed-types list, it can be marked as completed if all it's children are, but unless exactly that happens, it will not have it's progress\_percent saved. That is *unless* it is already started of it's own accord. If the parent is already started, then when a child is updated, the progress\_percent value of the parent will be edited as per any other content of a type allowed as per the allowed-for-started config settings.

Validation
----------

[](#validation)

### Non-technical Introduction

[](#non-technical-introduction)

This is business rules validation that helps ensure content requiring specific information|components is protected from *not* meeting said requirements. For example, you may have a state like "published", which you want only assigned to content this is ready for viewing by your customers. You don't want to accidentally launch content that is missing significant components and would reflect poorly on the brand. Thus, rules can be set within the Content Management System (CMS) to help ensure content meets set criteria.

Without validation, all edits are persisted "unchecked" - even if nonsensical\*. This can produce a site with incomplete content, and thus a poor user experience. To prevent this, rules can be configured by technical administrators. Edits to content will - as they're submitted - be "checked" against these set rules. That is, they will be "validated".

When initially creating a piece of content, we need to assume a period where it will fail all validation - as all the fields will empty! Thus, "states" such as "draft", "scheduled", and "published" can be set on content. The Validation rules are then set to only apply to certain "protected" states - typically "scheduled", and "published".

Knowing this, we can now look at the two situations which trigger validation:

1. Request to change a content's state to a "protected" state.
2. A content has a "protected" state, any details changes will trigger validation.

In each of the above cases, a validation *failure* will result (respectively) in:

1. the requested change to a protected state will not be executed - the state will remain unchanged. Note that if the current|previous state was a protected or unprotected state is irrelevant. All that happens is the requested state change is not persisted.
2. the requested change will not be made. To do so would result in having content that breaks the business rules for its type.

\* except for Musora CMS, for Musora Media, 2018, where currently video IDs, and exercise IDs are first validated by another system that will not enable setting a nonsensical value, regardless of content state.

### Technical Introduction

[](#technical-introduction)

This is *"business rules"* validation. This does not validate that the content is suitable for the the database. That is handled by rules hard-coded into a "rules" method in entity-and-action-specific `Request` classes (extension of `Railroad\Railcontent\Requests\FormRequest`, all in */src/Requests*). Rather, this "business rules" validation is used when content unique to your application requires specific infomation according to it's intended use within your domain. For example, for consumable consumer education and entertainment, you may have states like "public" or "published", which you want only assigned to content that has all components required to meet your requirements for quality and completeness.

In your application's config/ directory, you should have a railcontent.php file. In there under 'validation', you can list the brands for which you want validation. If a brand is not present, validation will not run.

The rules are organized thusly:

brand -&gt; content type -&gt; protected states -&gt; rules\*

\* see "About Defining Rules" section below

### About Defining Rules

[](#about-defining-rules)

The key for each content-type **must** be the same as the string representing content-type used in the application. Each content-type's rules has two required components, and can have an additional optional component:

1. fields
2. data
3. number\_of\_children (optional)

If a content-type exists in these rules, then validation will run as described above. If the content-type is not represented in the rules, the validation rules will not protect that content type according to the rules.

*(There is not currently a way to provide custom "restrictions" for a select content-type. See the "todo.md" file for notes about how to change this package to make that possible.)*

### Note about field or datum that reference another piece of content

[](#note-about-field-or-datum-that-reference-another-piece-of-content)

When a content has a field or datum that is itself a reference to another piece of content, the id of that referenced content is what is evaluated.

Thus, your rule must accommodate|use this.

For example: A video has a field with the id of a video... and that video is a piece of content itself. The rule might look like this:

```
'video' => [
    'rules' => [
        Rule::exists($connectionMaskPrefix . 'content', 'id')->where(
            function ($query) { $query->where('type', 'vimeo-video'); }
        ),
        'required'
    ]
],
```

This is done using the following logic within the "validateContent" method of the "\\Railroad\\Railcontent\\Requests\\**CustomFormRequest**" class:

```
if (($contentProperty['type'] ?? null) === 'content' && isset($inputToValidate['id'])) {
    $inputToValidate = $inputToValidate['id'];
}
```

You can see that if the `($contentProperty['type']` is "content" and the

### Important Note about the "numeric" rule

[](#important-note-about-the-numeric-rule)

```
If you don't include the "numeric" rule here, this doesn't work. I don't know why and it
doesn't make sense. Laravel' validator should - for the min, max, and size rules according to the
documentation - evaluate the evaluated value in regards to these rules based on the data-type
of the value under evaluation. If it's a string, it will evaluate the length. Thus, if min:5,
the string must have at least 5 characters. If the value is an integer min:5 will pass only if
the value of the integer is 5 or higher. If an array it evaluates the count. You get the idea.
But here it's failing to do that. Even if casting the value to an int just to be sure, and then
debugging to make sure it's not changed into a string somehow (because that would break it of
course because the string "6" would only pass for "min:1" and would fail for "min:2" and upwards
but the integer 6 would pass for "min:2" to "min:6". Maybe something about the "numeric" rule
is casting it to an int somewhere I didn't see. Anyways this is smelly af because that "numeric"
condition is just there as a hack to circumvent failure of the system to act as expected. But
it works, so fuck.

```

(^ Crudely written note copied here from comment in code)

### Specifying rules

[](#specifying-rules)

You can "...specify the validation rules as an array instead of using the | character to delimit them" ([from "Specifying A Custom Column Name" section here](https://laravel.com/docs/5.6/validation#rule-exists)).

For example; both `bar` and `bax` here would have the same effect:

```
foo => [
    'bar' => ['max:2|required'],
    'bax' => [
        'max:2',
        'required'
    ]
]
```

### Details of options available for each brand

[](#details-of-options-available-for-each-brand)

#### fields

[](#fields)

(required)

Array of rules keyed by the field they apply to. Each keyed value in this "fields" array represents one field. The value for each can be either a string or array representing the rules available in the Laravel version used. For details about Laravel rules, [see their documentation](https://laravel.com/docs/master/validation#available-validation-rules).

#### data

[](#data)

(required)

Same as fields, except data.

#### number\_of\_children

[](#number_of_children)

(optional)

See "**Important Note about the "numeric" rule**" note above.

Provide this in cases where a content type requires a set number of children before it can be set to a "restricted" state (perhaps "published" or "scheduled").

Can be defined either of the two ways:

```
'number_of_children' => 'min:3',
'number_of_children' => ['rules' => 'min:3'],
```

Unlike the rest of the rules, this doesn't *need* to be an item called "rules" in an array. It can be, or not. Either way will work.

### Configuration Example

[](#configuration-example)

```
'validation' => [
    'brand' => [
        'content-type' => [
            'content-status|content-status|...' => [
                'number_of_children' => 'min:3',
                'fields' => [
                    'key' => [
                        rules => ['validation-rule', 'validation-rule', ...],
                        'can_have_multiple' => true/false],
                    ],
                    'key' => [
                        rules => 'validation-rule|validation-rule|...',
                        'can_have_multiple' => true/false],
                    ],
                ],
                'data' => [
                    'key' => [
                        rules => ['validation-rule', 'validation-rule', ...],
                        'can_have_multiple' => true/false],
                    ],
                    'key' => [
                        rules => 'validation-rule|validation-rule|...',
                        'can_have_multiple' => true/false],
                    ],
                ]
            ]
        ],
    ]
]
```

### MultipleColumnExistsValidator

[](#multiplecolumnexistsvalidator)

***NOTE**: This actually doesn't work properly right now, so if the above may not quuuuite be accurate.*

*Is ^ this still accurate? (Jonathan, July 2018)*

You may not be able to use the `Rule::Exists()` feature of the Laravel's validator where you specify your validation rules. Perhaps you specify your rules in configuration files that are cache calling a method statically like that breaks them for some reason.

To overcome this, there exists a custom rule in this package called (Railroad\\Railcontent\\Validators) "MultipleColumnExistsValidator". Also, there is a test for it: (Tests\\Feature) "MultipleColumnExistsRuleTest".

Look at the class to understand fully how it works, if need be. If you only need a quick look, this is what it does:

```
$exists = $this->databaseManager->connection($connection)
    ->table($table)
    ->where($row, $value)
    ->exists();
```

You pass these arguments via your rules:

```
$connection = $parameterParts[0];
$table = $parameterParts[1];
$row = $parameterParts[2];
$value = isset($parameterParts[3]) ? $parameterParts[3] : $value;
```

A configured rule looks like this:

```
'video' => 'exists_multiple_columns:mysql,railcontent_content,id&mysql,railcontent_content,type,vimeo-video'

```

unpacked we get this:

```
exists_multiple_columns:
    mysql,railcontent_content,id
    &
    mysql,railcontent_content,type,vimeo-video

```

Breaking down each line:

`exists_multiple_columns` is the name of the custom rule

`mysql,railcontent_content,id` is a set or rules for a single "where exists" clause to the database validation that will run.

`&` (ampersand) is our delimiter - since the pipe, or vertical bar (`|`) is already used by laravel to delimit rules.

`mysql,railcontent_content,type,vimeo-video` is another \*"where exists" clause added to the database validation that will run when all clauses are compiled.

For the validation to pass in this case, both *"where exists" validations* must pass.

Within each *"where exists" validation*, values are delimited by commas (`,`).

So, the first set has THREE (3) values

```
mysql
railcontent_content
id

```

And the second set has FOUR (4) values:

```
mysql
railcontent_content
type
vimeo-video

```

The first THREE (3) parameters are required, the FOURTH (4th)) is optional.

As you may have guessed by the code snippet above, the parameters are as such:

1. connection (required)
2. table (required)
3. $row (required)
4. $value (optional)

If the FOURTH (4th) parameter is not passed, the value used for the "where exists" evaluation will be the value which the whole rule is validating.

Reviewing the example, we see that we're validating the value submitted to the `video` input against the rule `'exists_multiple_columns:mysql,railcontent_content,id&mysql,railcontent_content,type,vimeo-video'` to ensure that the value matches the two clauses `mysql,railcontent_content,id` and `mysql,railcontent_content,type,vimeo-video`. The first ensures that by the connection "mysql", in the table "railcontent\_content", the value under validation matches a value in the "id" column. The second does the same, but the value isn't the values passed in, but rather the value hard-coded in rule as the FOURTH parameter - "vimeo-video". Thus, it ensures that there exists a record where the value passes in matches something in the "id" column **and** at least one records matching that also has value or "vimeo-video" in it's "type" column.

#### WHERE *AND*

[](#where-and)

The above example describes a "WHERE ... AND ... " query. This is standard behaviour - everything must be present in at least one row for the validation to pass.

#### WHERE *OR*

[](#where-or)

This describes a "WHERE ... **OR** ... " query.

You can prepend your "clause rule string" (remember these are demarcated from each other with ampersands(&amp;)) with `or:`, so that for the examples above, the second one instead of looking like this:

```
mysql,railcontent_content,type,vimeo-video

```

would look like this

```
or:mysql,railcontent_content,type,vimeo-video

```

This works with the preceding rule and when there exists an `or:` "between" them, they're connected is an or statement.

Note that because you can have more than two "clause rule strings" in a rule, you can combine ORs and ANDs thusly:

```
exists_multiple_columns:
    mysql,railcontent_content,id
    &
    or:mysql,railcontent_content,foo_id
    &
    mysql,railcontent_content,type,vimeo-video
    &
    or:mysql,railcontent_content,slug,'bar baz'

```

Means that one of the first two "clause rule strings" must match with one of the two last "clause rule strings".

```
(
    mysql,railcontent_content,id
    OR
    mysql,railcontent_content,foo_id
)
AND
(
    mysql,railcontent_content,type,vimeo-video
    OR
    mysql,railcontent_content,slug,'bar baz'
)

```

###  Health Score

54

—

FairBetter than 97% of packages

Maintenance57

Moderate activity, may be stable

Popularity29

Limited adoption so far

Community18

Small or concentrated contributor base

Maturity95

Battle-tested with a long release history

 Bus Factor2

2 contributors hold 50%+ of commits

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 ~5 days

Total

510

Last Release

350d ago

Major Versions

v0.5.191 → v2.0.22022-09-13

v2.1.92 → v3.0.02024-08-13

v2.1.93 → v3.0.92024-09-20

v2.1.97 → v3.0.102024-11-26

v2.1.x-dev → v3.0.122024-12-02

PHP version history (4 changes)v0.0.2PHP ~7

v1.0.4PHP ~7.1

v2.0.0PHP ^8.1

v3.0.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/205e2d39157a11094e112cd3b2433bdc3449a3c30d67769f322b33169c3b8c70?d=identicon)[calebfavor](/maintainers/calebfavor)

---

Top Contributors

[![calebfavor](https://avatars.githubusercontent.com/u/5841780?v=4)](https://github.com/calebfavor "calebfavor (220 commits)")[![JonathanTM](https://avatars.githubusercontent.com/u/5752974?v=4)](https://github.com/JonathanTM "JonathanTM (203 commits)")[![roxanariza](https://avatars.githubusercontent.com/u/11618485?v=4)](https://github.com/roxanariza "roxanariza (167 commits)")[![CurtisConway](https://avatars.githubusercontent.com/u/21203902?v=4)](https://github.com/CurtisConway "CurtisConway (1 commits)")

---

Tags

cmsdeveloperrailcontent

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/railroad-railcontent/health.svg)

```
[![Health](https://phpackages.com/badges/railroad-railcontent/health.svg)](https://phpackages.com/packages/railroad-railcontent)
```

###  Alternatives

[backpack/crud

Quickly build admin interfaces using Laravel, Bootstrap and JavaScript.

3.4k3.4M207](/packages/backpack-crud)[shopware/platform

The Shopware e-commerce core

3.3k1.5M3](/packages/shopware-platform)[magento/community-edition

Magento 2 (Open Source)

12.1k52.1k10](/packages/magento-community-edition)[area17/twill

Twill is an open source CMS toolkit for Laravel that helps developers rapidly create a custom admin console that is intuitive, powerful and flexible.

4.0k445.4k16](/packages/area17-twill)[wheelpros/fitment-platform-api

Magento 2 (Open Source)

12.1k1.2k](/packages/wheelpros-fitment-platform-api)[unopim/unopim

UnoPim Laravel PIM

9.4k1.8k](/packages/unopim-unopim)

PHPackages © 2026

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