{"id":21909711,"url":"https://github.com/hyvor/phrosemirror","last_synced_at":"2026-03-03T04:40:02.144Z","repository":{"id":62382038,"uuid":"559930966","full_name":"hyvor/phrosemirror","owner":"hyvor","description":"Prosemirror in PHP","archived":false,"fork":false,"pushed_at":"2025-04-18T09:20:49.000Z","size":124,"stargazers_count":6,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-28T05:30:10.313Z","etag":null,"topics":["php","prosemirror"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyvor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-31T11:59:05.000Z","updated_at":"2025-07-10T22:13:23.000Z","dependencies_parsed_at":"2023-02-04T08:31:48.731Z","dependency_job_id":"a9592750-71cb-468a-8df6-c3f6d776b078","html_url":"https://github.com/hyvor/phrosemirror","commit_stats":{"total_commits":10,"total_committers":1,"mean_commits":10.0,"dds":0.0,"last_synced_commit":"b6591c91c5a6fac94c17bf3225a4821d37f7a172"},"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/hyvor/phrosemirror","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyvor%2Fphrosemirror","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyvor%2Fphrosemirror/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyvor%2Fphrosemirror/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyvor%2Fphrosemirror/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyvor","download_url":"https://codeload.github.com/hyvor/phrosemirror/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyvor%2Fphrosemirror/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30032061,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T03:27:35.548Z","status":"ssl_error","status_checked_at":"2026-03-03T03:27:09.213Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["php","prosemirror"],"created_at":"2024-11-28T17:25:44.885Z","updated_at":"2026-03-03T04:40:02.098Z","avatar_url":"https://github.com/hyvor.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"Phrosemirror is a PHP library to work with Prosemirror (or TipTap) JSON content in an easy and type-safe way.\n\nHere is what this library can do:\n\n* Convert Prosemirror JSON into a Document with typed Nodes, Marks, and Attributes\n* Analyze and change Documents\n* Convert a Document to HTML\n* Convert a Document to Text\n* Parse HTML to a Document\n* `content` and `group` for more strict schema conformity\n\n## Installation\n\n```\ncomposer require hyvor/phrosemirror\n```\n\n## 1. Schema\n\nThis library is unopinionated, which means there is no default schema. To start, you have to start with defining your schema that is similar to your front-end Prosemirror configurations. \n\n\u003e You can find an example schema in the `/example` directory in this repo, which is similar to `prosemirror-schema-basic` package's schema.\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\Schema;\n\n$schema = new Schema(\n    [\n        new Doc,\n        new Text,\n        new Paragraph,\n        new Blockquote,\n        new Image\n    ],\n    [\n        new Strong,\n        new Italic,  \n    ]\n);\n```\n\nIn the `Schema` constructor, first argument is an array of **Nodes Types** and the second one is an array of **Marks Types**.\n\n### Node Types\n\nA basic node type looks like this:\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\n\nclass Doc extends NodeType\n{\n    public string $name = 'doc';\n    public ?string $content = 'block+';\n}\n```\n\nThey can contain `content` and `group` properties. If `content` is not set, no content is allowed in this node. See [Content \u0026 Grouping](#content--grouping) below for more information on how these properties work.\n\nHere is another example of a Node Type:\n\n```php\nclass Paragraph extends NodeType\n{\n    public string $name = 'paragraph';\n    public ?string $content = 'inline*';\n    public string $group = 'block';\n}\n```\n\n\n### Mark Types\n\nA basic mark type looks like this:\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\MarkType;\n\nclass Strong extends MarkType\n{\n    public string $name = 'strong';\n}\n```\n\n### Attributes (Attrs)\n\nOne main goal of this library is to achieve type-safety. Therefore, attributes are defined in a typed class.\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\AttrsType;\n\nclass ImageAttrs extends AttrsType\n{\n\n    public string $src;\n    public ?string $alt;\n    \n}\n```\n\n\u003e By defining explicit types, we are sure that `src` attribute of the Image is always a string. `alt` can be a string or null.\n\nYou can also define default values for attributes, which will be used if they are not present in the JSON document.\n\n```php\nclass ImageAttrs extends AttrsType\n{\n    public string $src = 'https://hyvor.com/placeholder.png';\n}\n```\n\nThen, in the Node Type or Mark Type, you have to mention the Attrs class.\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\n\nclass Image extends NodeType\n{\n    // ...\n    public string $attrs = ImageAttrs::class;\n}\n```\n\n## 2. Document\n\nOnce the Schema is ready, we can start working with Documents.\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\Schema;\nuse Hyvor\\Phrosemirror\\Document\\Document;\n\n$schema = new Schema($nodes, $marks);\n$json = '{}'; // \u003c- this is the JSON from the front-end\n\n$document = Document::fromJson($schema, $json);\n```\n\n`$json` can be a JSON string, a PHP array, or a PHP object. If the given JSON is valid, `$document` will be an instance of `Hyvor\\Phrosemirror\\Document\\Document`. If not, an error will be thrown. See Error Handling below.\n\n### Node\n\nA `Document` is just a `Node` with the `doc` type. These are the properties of a Node.\n\n```php\nnamespace Hyvor\\Phrosemirror\\Document;\n\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\nuse Hyvor\\Phrosemirror\\Types\\AttrsType;\n\nclass Node\n{\n    public NodeType $type;\n    public AttrsType $attrs;\n    public Fragment $content;\n    public Mark[] $marks;\n}\n```\n\n`NodeType $type` is the type of the node, which you defined in the schema\n\n`AttrsType $attrs` is the attributes of the Node. This will be an object of the class you defined in Node Type attrs. For example, as in the above example of Node Types, if the node is `Image`, `$attrs` will be an object of `ImageAttrs`.\n\n`Fragment $content` is a collection of children Nodes.\n\n`Mark[] $marks` is an array of Marks assigned to this node.\n\n\u003e `TextNode` is a special `Node` that represents the `text` node type in Prosemirror. It has the `string $text` property in addition to the above properties. Also, `$marks` only makes sense in the context of `TextNode`.\n\n#### Checking Node Type\n\nUse `isOfType()` to check if a `Node` is of a particular `NodeType` defined in your schema.\n\n```php\n$json = ['type' =\u003e 'paragraph'];\n$node = Node::fromJson($schema, $json);\n\n$node-\u003eisOfType(Paragraph::class); // true\n$node-\u003eisOfType(Image::class); // false\n$node-\u003eisOfType([Paragraph::class, Image::class]); // true\n```\n\n#### Accessing Attributes\n\nUse the `attr()` method to access an attribute of the Node.\n\n```php\n$json = ['type' =\u003e 'image', 'attrs' =\u003e ['src' =\u003e 'image.png']];\n$image = Node::fromJson($schema, $json);\n\n// html-escaped (safe to use in HTML output)\n$src = $image-\u003eattr('src');\n\n// not html-escaped\n$src = $image-\u003eattr('src', escape: false);\n```\n\n#### Traversing Through Nested Nodes\n\nYou can traverse through nested nodes using the `traverse()` method with a callback. Here is an example that traverse through all nodes and finds all image nodes.\n\n```php\n$document = Document::fromJson($schema, $json);\n\n$images = [];\n$document-\u003etraverse(function(Node $node) use(\u0026$images) {\n    if ($node-\u003eisOfType(Image::class)) {\n        $images[] = $node;\n    }\n})\n```\n\n\u003e `traverse()` traverses through `TextNode`s too!\n\n#### Traversing Through Direct Children\n\nUse `foreach` with `$node-\u003econtent`.\n\n```php\nforeach ($node-\u003econtent as $child) {\n    if ($child-\u003eisOfType(Image::class)) {\n        echo \"I found an image!\";\n    }\n}\n```\n\n#### Finding Nodes\n\nEarlier, we used `traverse()` to find nodes, but there is the `getNodes()` method to make it easier. It searches through the all nested nodes and returns `Node[]` of matched nodes.\n\n```php\n// images\n$node-\u003egetNodes(Image::class);\n\n// all nodes (including TextNodes)\n$node-\u003egetNodes();\n\n// nodes of multiple types\n$node-\u003egetNodes([Paragraph::class, Blockquote::class]);\n\n// images (only direct children)\n$node-\u003egetNodes(Image::class, false);\n```\n\n#### Finding Marks\n\nSimilar to `getNodes()` you can use `getMarks()` to find marks within the current node. It searches all nested nodes and returns `Mark[]` of matched marks.\n\n```php\n// links\n$node-\u003egetMarks(Link::class);\n\n// all marks\n$node-\u003egetMarks();\n\n// multiple types\n$node-\u003egetMarks([Strong::class, Italic::class]);\n\n// without nesting (marks of the current node only)\n$node-\u003egetMarks(Link::class, false);\n```\n\n#### JSON Serialize\n\nYou can serialize a Node/Document back to JSON.\n\n```php\n$node-\u003etoJson(); // JSON string\n$node-\u003etoArray(); // PHP array\n```\n\n### Mark\n\n```php\nnamespace Hyvor\\Phrosemirror\\Document;\n\nuse Hyvor\\Phrosemirror\\Types\\MarkType;\nuse Hyvor\\Phrosemirror\\Types\\AttrsType;\n\nclass Mark\n{\n    public MarkType $type;\n    public AttrsType $attrs;  \n}\n```\n\n`$type` and `$attrs` are analogous to those of Node's.\n\n`Mark` has `isOfType()`, `attr()`, `toArray()`, and `toJson()`, which works similar to `Node`'s methods.\n\n```php\n$mark = Mark::fromJson(['type' =\u003e 'link', 'attrs' =\u003e ['src' =\u003e 'https://hyvor.com']);\n\n$mark-\u003eisOfType(Strong::class); // false\n$mark-\u003eattr('src'); // https://hyvor.com\n```\n\n### Fragment\n\n`$node-\u003econtent` is a `Fragment`. It contains an array of children nodes. You can think of it just as an array, but with helper methods that makes things easier.\n\n```php\n$fragment = $node-\u003econtent();\n\n// READ\n\n$fragment-\u003efirst(); // Node | null\n$fragment-\u003elast(); // Node | null\n$fragment-\u003enth(2); // Node | null\n\n$fragment-\u003ecount(); // int\n\n// get all Nodes in the Fragment as an array\n$fragment-\u003eall(); // Node[]\n\n// loop through each node\n$fragment-\u003eeach(fn (Node $node) =\u003e false);\n\n// WRITE (Be careful, these methods changes the document)\n\n$fragment-\u003eaddNodeToStart($node);\n$fragment-\u003eaddNodeToEnd($node);\n$fragment-\u003eaddNode($node); // same as addNodeToEnd\n$fragment-\u003esetNodes($nodes);\n$fragment-\u003emap(fn (Node $node) =\u003e $node); // update nodes in a callback\n```\n\n## 3. HTML\n\nNext, let's convert your document to HTML. To do this, you have to define the `toHtml()` method in Node Types and Mark Types.\n\n```php\nuse Hyvor\\Phrosemirror\\Document\\Node;\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\n\nclass Paragraph extends NodeType\n{\n\n    public function toHtml(Node $node, string $children) : string\n    {\n        return \"\u003cp\u003e$children\u003c/p\u003e\";\n    }\n\n}\n```\n\n`toHtml()` should return the HTML string of the node, placing the `$children` string in it.\n\nHere is another example using the attributes of that Node.\n\n```php\nuse Hyvor\\Phrosemirror\\Document\\Node;\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\n\nclass Image extends NodeType\n{\n\n    public function toHtml(Node $node, string $children) : string\n    {\n        $src = $node-\u003eattr('src');\n        return \"\u003cimg src=\\\"$src\\\"\u003e$children\u003c/p\u003e\";\n    }\n\n}\n```\n\n\u003e Do not directly use `$node-\u003eattrs-\u003esrc` as the raw attributes are not HTML-escaped. Always use `$node-\u003eattr()` or `$node-\u003eattrs-\u003eget()`\n\n### HTML: Document -\u003e HTML\n\nUse the `toHtml()` method to serialize a document (or any node) to HTML. \n\n```php\n$document = Document::fromJson($schema, $json);\n$html = $document-\u003etoHtml();\n```\n\n## Parsing HTML\n\nThe `HtmlParser` class is responsible for parsing HTML to a Document. It takes the Schema and some parsing rules to parse the HTML.\n\n```php\n\u003c?php\nuse Hyvor\\Phrosemirror\\Converters\\HtmlParser\\HtmlParser;use Hyvor\\Phrosemirror\\Converters\\HtmlParser\\ParserRule;\n\n$schema = new Schema($nodes, $marks); // this is the same schema you create for the document\n$parser = new HtmlParser($schema, [\n    new ParserRule(tag: 'p', node: 'paragraph'),\n    new ParserRule(tag: '#text', node: 'text'),\n    // ... other rules\n])\n$doc = $parser-\u003eparse($html);\n```\n\nHowever, in most cases, you only need one rule set to parse from multiple HTML inputs. Therefore, you can directly define rules in the Schema (in the `fromHtml()` method of Nodes and Marks).\n\n```php\nuse Hyvor\\Phrosemirror\\Types\\NodeType;\nuse Hyvor\\Phrosemirror\\Converters\\HtmlParser\\ParserRule;\nuse Hyvor\\Phrosemirror\\Document\\Node;\n\nclass Paragraph extends NodeType\n{\n\n    public string $name = 'paragraph';\n    public ?string $content = 'inline*';\n    public string $group = 'block';\n\n    public function toHtml(Node $node, string $children): string\n    {\n        return \"\u003cp\u003e$children\u003c/p\u003e\";\n    }\n\n    public function fromHtml(): array\n    {\n        return [\n            new ParserRule(tag: 'p'),\n        ];\n    }\n\n}\n```\n\nThe `fromHtml()` method should return `ParserRule[]`. Here, the `node` property is not required as it is the same as the Node Type's name.\n\nThen, use the `fromSchema()` method to create the parser.\n\n```php\n$parser = HtmlParser::fromSchema($schema);\n$doc = $parser-\u003eparse($html);\n```\n\n### Parsing HTML Attributes to Node Attributes\n\nUse the `getAttrs()` method to parse attributes from the HTML element.\n\n```php\nuse DOMElement;\n\nclass Image extends NodeType\n{\n    public string $name = 'image';\n    public string $attrs = ImageAttrs::class;\n    \n    public function fromHtml() : array\n    {\n    \n        return [\n            new ParserRule(\n                tag: 'img', \n                getAttrs: fn (DOMElement $element) =\u003e ImageAttrs::fromArray([\n                    'src' =\u003e $element-\u003egetAttribute('src'),\n                    'alt' =\u003e $element-\u003egetAttribute('alt'),\n                ])\n            )\n        ];\n    \n    }\n}\n```\n\nThe `getAttrs()` callback should return one of the following:\n\n- `false` to ignore the element\n- `null` if the attributes are not found\n- `AttrsType` if the attributes are found\n\n\u003c!--\n### Parsing Using Styles\n\nYou can also parse HTML elements using their styles instead of the tag.\n\n```php\n\n```\n--\u003e\n\n### Content \u0026 Grouping\n\nDefining `content` and `group` properties in Node Types is important for parsing HTML. \n\nFor example, let's say we have a `blockquote` node with `content` set to `block+`. This means that the `blockquote` node can only contain block nodes. Therefore, the following HTML does not conform to the schema.\n\n```html\n\u003cblockquote\u003eHello World\u003c/blockquote\u003e\n```\n\nThis is when `content` and `group` properties come in handy. Because, we know that the `blockquote` node can only contain block nodes such as paragraphs, the HTML parser will automatically wrap the text in a paragraph when parsing. The resulting HTML would be:\n\n```html\n\u003cblockquote\u003e\u003cp\u003eHello World\u003c/p\u003e\u003c/blockquote\u003e\n```\n\nThis logic is handled by `Sanitizer` class. Simply, it does the following to ensure `content` and `group` conformity:\n\n* Tries to wrap nodes\n* Tries to promote children\n* Tries to connect inline nodes\n* If all fails, it will remove the node\n\n`content` expressions support everything the Prosemirror front-end library supports. Here are some examples:\n\n* `paragraph`\n* `paragraph|heading`\n* `paragraph?`\n* `paragraph*`\n* `paragraph+`\n* `paragraph{1,3}`\n* `paragraph{1,}`\n* `paragraph heading` (subsequent nodes)\n* `paragraph (heading | code_block)+`\n* `block+` (using groups)\n\n**Note**: This sanitization process is only run when parsing a document from HTML. It is not run when parsing a document from JSON, because we expect the JSON (usually from your front-end) to be valid. However, you can still run the sanitization process as follows if needed:\n\n```php\n$doc = Document::fromJson($schema, $json);\n$sanitizedDoc = Sanitizer::sanitize($schema, $doc);\n```\n\n### Disabling Sanitization\n\nYou can disable `content` sanitization when parsing HTML by setting `sanitize: false`.\n\n```php\n$parser = HtmlParser::fromSchema($schema);\n$doc = $parser-\u003eparse($html, sanitize: false);\n```\n\n\u003e ⚠️ **Warning:** Disabling sanitization can result in invalid documents.\n\n## Error Handling\n\nThis library is strict, and it expects correct input from the front-end. It can throw the following exceptions:\n\n* `InvalidJsonException` - on invalid JSON\n* `InvalidAttributeTypeException` - on invalid attribute type\n\nBoth exceptions extend the `PhrosemirrorException` class. Therefore, the best practise would be catching it when building the document.\n\n```php\nuse Hyvor\\Phrosemirror\\Exception\\PhrosemirrorException;\n\ntry {\n    $document = Document::fromJson($schema, $json);\n} catch (PhrosemirrorException $e) {\n    // invalid document\n}\n```\n\nIf the front-end (JS) and back-end (PHP) schema matches, the only way an exception can happen is when the Prosemirror JSON is altered. Therefore, it is a good practise to stop processing here.\n\n## Who uses this Library?\n\n* [Hyvor Talk](https://talk.hyvor.com)\n* [Hyvor Blogs](https://blogs.hyvor.com)\n* Add yours with a PR","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyvor%2Fphrosemirror","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyvor%2Fphrosemirror","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyvor%2Fphrosemirror/lists"}