├── .gitignore ├── README.md ├── resources ├── assets │ ├── .gitignore │ ├── dist │ │ └── admin-color-scheme │ │ │ ├── assets │ │ │ └── admin-color-scheme-MNJzxSi2.js │ │ │ └── .vite │ │ │ └── manifest.json │ ├── admin-color-scheme.js │ ├── package.json │ ├── vite.config.color-scheme.js │ └── admin-color-scheme │ │ ├── _mixins.scss │ │ ├── scheme.scss │ │ └── _variables.scss └── views │ ├── partials │ └── feed-item.php │ └── templates │ └── feed.php ├── src ├── Fields │ └── Acf │ │ ├── ColorField.php │ │ ├── EmailField.php │ │ ├── OEmbedField.php │ │ ├── PasswordField.php │ │ ├── UrlField.php │ │ ├── TabField.php │ │ ├── GoogleMapField.php │ │ ├── TimeField.php │ │ ├── FlexibleContentLayout.php │ │ ├── TextField.php │ │ ├── GroupField.php │ │ ├── RangeField.php │ │ ├── DateField.php │ │ ├── BooleanField.php │ │ ├── FileField.php │ │ ├── NumberField.php │ │ ├── MessageField.php │ │ ├── TextAreaField.php │ │ ├── WysiwygField.php │ │ ├── GalleryField.php │ │ ├── PostObjectField.php │ │ ├── FlexibleContentField.php │ │ ├── CheckboxField.php │ │ ├── SelectField.php │ │ ├── RepeaterField.php │ │ ├── RadioField.php │ │ ├── DateTimeField.php │ │ ├── TaxonomyField.php │ │ ├── RelationshipField.php │ │ ├── ImageField.php │ │ ├── Field.php │ │ └── Properties │ │ └── HasSubFields.php ├── Services │ ├── ServiceInterface.php │ ├── Cache │ │ ├── CacheInterface.php │ │ ├── AbstractCache.php │ │ └── FileCache.php │ ├── SitemapRenderer.php │ ├── Sitemap.php │ └── Rss.php ├── Utils │ ├── Http.php │ ├── File.php │ ├── Date.php │ ├── Arr.php │ ├── Filesystem.php │ ├── Register.php │ ├── Obj.php │ ├── Acf.php │ ├── OEmbed.php │ └── Link.php ├── functions.php ├── Attachment.php ├── Terms.php ├── Settings │ ├── Api.php │ ├── Html.php │ ├── Wp.php │ └── Language.php ├── Posts.php ├── User.php ├── Taxonomy.php ├── Term.php ├── PostType.php ├── Support │ ├── FluentList.php │ └── Fluent.php ├── Tooling │ └── Vite.php └── Post.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Work in progress. 2 | -------------------------------------------------------------------------------- /resources/assets/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /resources/assets/dist/admin-color-scheme/assets/admin-color-scheme-MNJzxSi2.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Fields/Acf/ColorField.php: -------------------------------------------------------------------------------- 1 | placeholder = $text; 12 | return $this; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev-color-scheme": "vite --config vite.config.color-scheme.js", 6 | "build-color-scheme": "vite build --config vite.config.color-scheme.js", 7 | "upgrade": "pnpm dlx npm-check-updates -u && pnpm install" 8 | }, 9 | "devDependencies": { 10 | "sass": "^1.69.5", 11 | "vite": "^5.0.5", 12 | "vite-plugin-live-reload": "^3.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Fields/Acf/TabField.php: -------------------------------------------------------------------------------- 1 | layout = 'table'; 14 | // return $this; 15 | // } 16 | 17 | public function endpoint(bool $endpoint = true): self 18 | { 19 | $this->endpoint = $endpoint; 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Fields/Acf/GoogleMapField.php: -------------------------------------------------------------------------------- 1 | ini_get('date.default_latitude') ?: '', 16 | 'center_lng' => ini_get('date.default_longitude') ?: '', 17 | ]; 18 | parent::__construct($name, array_merge($defaults, $settings)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Fields/Acf/TimeField.php: -------------------------------------------------------------------------------- 1 | display_format = $format; 17 | return $this; 18 | } 19 | 20 | public function returnFormat(string $format): self 21 | { 22 | $this->return_format = $format; 23 | return $this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Fields/Acf/FlexibleContentLayout.php: -------------------------------------------------------------------------------- 1 | sub_fields[] = $field; 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Services/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | placeholder = $text; 12 | return $this; 13 | } 14 | 15 | public function maxLength(int $chars): self 16 | { 17 | $this->maxlength = $chars; 18 | return $this; 19 | } 20 | 21 | public function prepend(string $label): self 22 | { 23 | $this->prepend = $label; 24 | return $this; 25 | } 26 | 27 | public function append(string $label): self 28 | { 29 | $this->append = $label; 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Fields/Acf/GroupField.php: -------------------------------------------------------------------------------- 1 | sub_fields[] = $field; 17 | return $this; 18 | } 19 | 20 | public function layoutTable(): self 21 | { 22 | $this->layout = 'table'; 23 | return $this; 24 | } 25 | 26 | public function layoutBlock(): self 27 | { 28 | $this->layout = 'block'; 29 | return $this; 30 | } 31 | 32 | public function layoutRow(): self 33 | { 34 | $this->layout = 'row'; 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Fields/Acf/RangeField.php: -------------------------------------------------------------------------------- 1 | step = $step; 13 | return $this; 14 | } 15 | 16 | public function min(float $min): self 17 | { 18 | $this->min = $min; 19 | return $this; 20 | } 21 | 22 | public function max(float $max): self 23 | { 24 | $this->max = $max; 25 | return $this; 26 | } 27 | 28 | public function prepend(string $label): self 29 | { 30 | $this->prepend = $label; 31 | return $this; 32 | } 33 | 34 | public function append(string $label): self 35 | { 36 | $this->append = $label; 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Fields/Acf/DateField.php: -------------------------------------------------------------------------------- 1 | display_format = $format; 21 | return $this; 22 | } 23 | 24 | public function returnFormat(string $format): self 25 | { 26 | $this->return_format = $format; 27 | return $this; 28 | } 29 | 30 | public function weekStarts(int $weekday): self 31 | { 32 | $this->first_day = $weekday; 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Fields/Acf/BooleanField.php: -------------------------------------------------------------------------------- 1 | ui_on_text = tx($label, 'fields', null, $written_language); 15 | return $this; 16 | } 17 | 18 | public function labelFalse( 19 | string $label, 20 | string $written_language = null 21 | ): self { 22 | $this->ui_off_text = tx($label, 'fields', null, $written_language); 23 | return $this; 24 | } 25 | 26 | public function message( 27 | string $message, 28 | string $written_language = null 29 | ): self { 30 | $this->message = tx($message, 'fields', null, $written_language); 31 | return $this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Fields/Acf/FileField.php: -------------------------------------------------------------------------------- 1 | return_format = 'id'; 17 | return $this; 18 | } 19 | 20 | public function returnUrl(): self 21 | { 22 | $this->return_format = 'url'; 23 | return $this; 24 | } 25 | 26 | public function returnArray(): self 27 | { 28 | $this->return_format = 'array'; 29 | return $this; 30 | } 31 | 32 | public function mimeTypes($extensions): self 33 | { 34 | if (is_array($extensions)) { 35 | $extensions = implode(',', $extensions); 36 | } 37 | $this->mime_types = (string) $extensions; 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/assets/vite.config.color-scheme.js: -------------------------------------------------------------------------------- 1 | 2 | import { defineConfig } from 'vite' 3 | import liveReload from 'vite-plugin-live-reload' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | 8 | plugins: [ 9 | // reloads the page when Bond files change 10 | liveReload('../../src/**/*.php', { 11 | root: __dirname, 12 | }), 13 | ], 14 | 15 | build: { 16 | // output dir for production build 17 | outDir: 'dist/admin-color-scheme', 18 | emptyOutDir: true, 19 | 20 | // emit manifest so PHP can find the hashed files 21 | manifest: true, 22 | 23 | rollupOptions: { 24 | // our entry 25 | input: 'admin-color-scheme.js', 26 | }, 27 | }, 28 | 29 | server: { 30 | // required to load scripts from custom host 31 | cors: true, 32 | 33 | // we need a strict port to match on PHP side 34 | strictPort: true, 35 | port: 2342, // if changed match on Settings/Admin 36 | }, 37 | 38 | }) 39 | -------------------------------------------------------------------------------- /src/Fields/Acf/NumberField.php: -------------------------------------------------------------------------------- 1 | step = $step; 12 | return $this; 13 | } 14 | 15 | public function min(float $min): self 16 | { 17 | $this->min = $min; 18 | return $this; 19 | } 20 | 21 | public function max(float $max): self 22 | { 23 | $this->max = $max; 24 | return $this; 25 | } 26 | 27 | public function prepend(string $label): self 28 | { 29 | $this->prepend = $label; 30 | return $this; 31 | } 32 | 33 | public function append(string $label): self 34 | { 35 | $this->append = $label; 36 | return $this; 37 | } 38 | 39 | public function placeholder(string $text): self 40 | { 41 | $this->placeholder = $text; 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Fields/Acf/MessageField.php: -------------------------------------------------------------------------------- 1 | message = tx($message, 'fields', null, $written_language); 17 | return $this; 18 | } 19 | 20 | public function lineBreakNone(): self 21 | { 22 | $this->new_lines = ''; 23 | return $this; 24 | } 25 | 26 | public function lineBreakBr(): self 27 | { 28 | $this->new_lines = 'br'; 29 | return $this; 30 | } 31 | 32 | public function lineBreakParagraph(): self 33 | { 34 | $this->new_lines = 'wpautop'; 35 | return $this; 36 | } 37 | 38 | public function escapeHtml(bool $active = true): self 39 | { 40 | $this->esc_html = $active; 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Fields/Acf/TextAreaField.php: -------------------------------------------------------------------------------- 1 | placeholder = $text; 13 | return $this; 14 | } 15 | 16 | public function maxLength(int $chars): self 17 | { 18 | $this->maxlength = $chars; 19 | return $this; 20 | } 21 | 22 | public function rows(int $lines): self 23 | { 24 | $this->rows = $lines; 25 | return $this; 26 | } 27 | 28 | public function lineBreakNone(): self 29 | { 30 | $this->new_lines = ''; 31 | return $this; 32 | } 33 | 34 | public function lineBreakBr(): self 35 | { 36 | $this->new_lines = 'br'; 37 | return $this; 38 | } 39 | 40 | public function lineBreakParagraph(): self 41 | { 42 | $this->new_lines = 'wpautop'; 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Fields/Acf/WysiwygField.php: -------------------------------------------------------------------------------- 1 | tabs = 'all'; 12 | return $this; 13 | } 14 | 15 | public function visualTabOnly(): self 16 | { 17 | $this->tabs = 'visual'; 18 | return $this; 19 | } 20 | 21 | public function textTabOnly(): self 22 | { 23 | $this->tabs = 'text'; 24 | return $this; 25 | } 26 | 27 | public function toolbar(string $id): self 28 | { 29 | $this->toolbar = $id; 30 | return $this; 31 | } 32 | 33 | public function mediaUpload(bool $active = true): self 34 | { 35 | $this->media_upload = $active; 36 | return $this; 37 | } 38 | 39 | public function delayInitialization(bool $active = true): self 40 | { 41 | $this->delay = $active; 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Fields/Acf/GalleryField.php: -------------------------------------------------------------------------------- 1 | return_format = 'id'; 17 | return $this; 18 | } 19 | 20 | public function returnUrl(): self 21 | { 22 | $this->return_format = 'url'; 23 | return $this; 24 | } 25 | 26 | public function returnArray(): self 27 | { 28 | $this->return_format = 'array'; 29 | return $this; 30 | } 31 | 32 | 33 | public function mimeTypes($extensions): self 34 | { 35 | if (is_array($extensions)) { 36 | $extensions = implode(',', $extensions); 37 | } 38 | $this->mime_types = (string) $extensions; 39 | return $this; 40 | } 41 | 42 | public function onlyJpg(): self 43 | { 44 | $this->mimeTypes('jpg,jpeg'); 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-bond/bond", 3 | "homepage": "https://github.com/wp-bond/bond", 4 | "description": "A utility library for WordPress developers.", 5 | "keywords": [ 6 | "wordpress", 7 | "utility", 8 | "library" 9 | ], 10 | "type": "library", 11 | "license": "GPL-3.0-or-later", 12 | "authors": [ 13 | { 14 | "name": "André Felipe", 15 | "homepage": "https://addd.studio" 16 | } 17 | ], 18 | "config": { 19 | "sort-packages": true 20 | }, 21 | "require": { 22 | "php": ">=8.0", 23 | "league/container": "^4.1", 24 | "mobiledetect/mobiledetectlib": "^2.8", 25 | "nesbot/carbon": "^2.50", 26 | "psr/simple-cache": "^1.0", 27 | "symfony/polyfill-mbstring": "^1.23", 28 | "symfony/polyfill-php81": "^1.23", 29 | "voku/portable-ascii": "^1.5" 30 | }, 31 | "autoload": { 32 | "files": [ 33 | "src/functions.php" 34 | ], 35 | "psr-4": { 36 | "Bond\\": "src/" 37 | } 38 | }, 39 | "suggest": { 40 | "google/cloud-translate": "Translate strings with Google Translate.", 41 | "aws/aws-sdk-php": "Translate strings with AWS Translate.", 42 | "symfony/var-dumper": "To use dd() function during development." 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Fields/Acf/PostObjectField.php: -------------------------------------------------------------------------------- 1 | return_format = 'id'; 19 | return $this; 20 | } 21 | 22 | public function returnObject(): self 23 | { 24 | $this->return_format = 'object'; 25 | return $this; 26 | } 27 | 28 | // array | string 29 | public function postType($post_types): self 30 | { 31 | $this->post_type = (array) $post_types; 32 | return $this; 33 | } 34 | 35 | // array | string 36 | public function taxonomy($taxonomies): self 37 | { 38 | $this->taxonomy = (array) $taxonomies; 39 | return $this; 40 | } 41 | 42 | public function allowNull(bool $active = true): self 43 | { 44 | $this->allow_null = $active; 45 | return $this; 46 | } 47 | 48 | public function multiple(bool $active = true): self 49 | { 50 | $this->multiple = $active; 51 | return $this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/Cache/AbstractCache.php: -------------------------------------------------------------------------------- 1 | enabled; 21 | } 22 | 23 | public function enable() 24 | { 25 | $this->enabled = true; 26 | } 27 | 28 | public function disable() 29 | { 30 | $this->enabled = false; 31 | } 32 | 33 | public function ttl(?int $seconds = null): int 34 | { 35 | if ($seconds !== null) { 36 | $this->ttl = $seconds; 37 | } 38 | return $this->ttl; 39 | } 40 | 41 | public function keyHash(string $key, string|array $hash): string 42 | { 43 | return $key 44 | . (!empty($hash) 45 | ? '-' . md5(Str::kebab($hash)) 46 | : ''); 47 | } 48 | 49 | protected function value($value) 50 | { 51 | return is_callable($value) ? $value() : $value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/assets/admin-color-scheme/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Button mixin- creates a button effect with correct 3 | * highlights/shadows, based on a base color. 4 | */ 5 | @mixin button( $button-color, $button-text-color: #fff ) { 6 | background: $button-color; 7 | border-color: $button-color; 8 | color: $button-text-color; 9 | 10 | &:hover, 11 | &:focus { 12 | background: lighten( $button-color, 3% ); 13 | border-color: darken( $button-color, 3% ); 14 | color: $button-text-color; 15 | } 16 | 17 | &:focus { 18 | box-shadow: 19 | 0 0 0 1px #fff, 20 | 0 0 0 3px $button-color; 21 | } 22 | 23 | &:active { 24 | background: darken( $button-color, 5% ); 25 | border-color: darken( $button-color, 5% ); 26 | color: $button-text-color; 27 | } 28 | 29 | &.active, 30 | &.active:focus, 31 | &.active:hover { 32 | background: $button-color; 33 | color: $button-text-color; 34 | border-color: darken( $button-color, 15% ); 35 | box-shadow: inset 0 2px 5px -3px darken( $button-color, 50% ); 36 | } 37 | 38 | &[disabled], 39 | &:disabled, 40 | &.button-primary-disabled, 41 | &.disabled { 42 | color: hsl( hue( $button-color ), 10%, 80% ) !important; 43 | background: darken( $button-color, 8% ) !important; 44 | border-color: darken( $button-color, 8% ) !important; 45 | text-shadow: none !important; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Fields/Acf/FlexibleContentField.php: -------------------------------------------------------------------------------- 1 | addLayout($field); 24 | return $field; 25 | } 26 | 27 | public function addLayout(FlexibleContentLayout $field): self 28 | { 29 | $this->layouts[] = $field; 30 | return $this; 31 | } 32 | 33 | public function buttonLabel( 34 | string $label, 35 | string $written_language = null 36 | ): self { 37 | $this->button_label = tx($label, 'fields', null, $written_language); 38 | return $this; 39 | } 40 | 41 | public function min(int $min): self 42 | { 43 | $this->min = $min; 44 | return $this; 45 | } 46 | 47 | public function max(int $max): self 48 | { 49 | $this->max = $max; 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Fields/Acf/CheckboxField.php: -------------------------------------------------------------------------------- 1 | choices = $choices; 14 | return $this; 15 | } 16 | 17 | public function allowCustom(bool $active = true): self 18 | { 19 | $this->allow_custom = $active; 20 | return $this; 21 | } 22 | 23 | public function selectAllToggle(bool $active = true): self 24 | { 25 | $this->toggle = $active; 26 | return $this; 27 | } 28 | 29 | public function layoutVertical(): self 30 | { 31 | $this->layout = 'vertical'; 32 | return $this; 33 | } 34 | 35 | public function layoutHorizontal(): self 36 | { 37 | $this->layout = 'horizontal'; 38 | return $this; 39 | } 40 | 41 | public function returnValue(): self 42 | { 43 | $this->return_format = 'value'; 44 | return $this; 45 | } 46 | 47 | public function returnLabel(): self 48 | { 49 | $this->return_format = 'label'; 50 | return $this; 51 | } 52 | 53 | public function returnArray(): self 54 | { 55 | $this->return_format = 'array'; 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Fields/Acf/SelectField.php: -------------------------------------------------------------------------------- 1 | choices = $choices; 15 | return $this; 16 | } 17 | 18 | public function allowNull(bool $active = true): self 19 | { 20 | $this->allow_null = $active; 21 | return $this; 22 | } 23 | 24 | public function multiple(bool $active = true): self 25 | { 26 | $this->multiple = $active; 27 | return $this; 28 | } 29 | 30 | public function ui(bool $active = true): self 31 | { 32 | $this->ui = $active; 33 | return $this; 34 | } 35 | 36 | public function ajax(bool $active = true): self 37 | { 38 | $this->ajax = $active; 39 | return $this; 40 | } 41 | 42 | public function returnValue(): self 43 | { 44 | $this->return_format = 'value'; 45 | return $this; 46 | } 47 | 48 | public function returnLabel(): self 49 | { 50 | $this->return_format = 'label'; 51 | return $this; 52 | } 53 | 54 | public function returnArray(): self 55 | { 56 | $this->return_format = 'array'; 57 | return $this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Utils/Http.php: -------------------------------------------------------------------------------- 1 | remember( 25 | 'bond/http/content-length-' . md5($url), 26 | function () use ($url) { 27 | 28 | $ch = curl_init(); 29 | curl_setopt($ch, CURLOPT_URL, $url); 30 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 31 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 32 | curl_setopt($ch, CURLOPT_HEADER, true); 33 | curl_setopt($ch, CURLOPT_NOBODY, true); 34 | curl_exec($ch); 35 | $size = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); 36 | curl_close($ch); 37 | 38 | return $size ?: null; 39 | }, 40 | -1 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Fields/Acf/RepeaterField.php: -------------------------------------------------------------------------------- 1 | sub_fields[] = $field; 20 | return $this; 21 | } 22 | 23 | 24 | public function buttonLabel( 25 | string $label, 26 | string $written_language = null 27 | ): self { 28 | $this->button_label = tx($label, 'fields', null, $written_language); 29 | return $this; 30 | } 31 | 32 | public function min(int $min): self 33 | { 34 | $this->min = $min; 35 | return $this; 36 | } 37 | 38 | public function max(int $max): self 39 | { 40 | $this->max = $max; 41 | return $this; 42 | } 43 | 44 | public function showWhenCollapsed(string $name): self 45 | { 46 | // TODO 47 | // $this->collapsed = 48 | return $this; 49 | } 50 | 51 | public function layoutTable(): self 52 | { 53 | $this->layout = 'table'; 54 | return $this; 55 | } 56 | 57 | public function layoutBlock(): self 58 | { 59 | $this->layout = 'block'; 60 | return $this; 61 | } 62 | 63 | public function layoutRow(): self 64 | { 65 | $this->layout = 'row'; 66 | return $this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Fields/Acf/RadioField.php: -------------------------------------------------------------------------------- 1 | choices = $choices; 17 | return $this; 18 | } 19 | 20 | public function allowNull(bool $active = true): self 21 | { 22 | $this->allow_null = $active; 23 | return $this; 24 | } 25 | 26 | public function otherChoice(bool $active = true): self 27 | { 28 | $this->other_choice = $active; 29 | return $this; 30 | } 31 | 32 | public function saveOtherChoice(bool $active = true): self 33 | { 34 | $this->save_other_choice = $active; 35 | return $this; 36 | } 37 | 38 | public function layoutVertical(): self 39 | { 40 | $this->layout = 'vertical'; 41 | return $this; 42 | } 43 | 44 | public function layoutHorizontal(): self 45 | { 46 | $this->layout = 'horizontal'; 47 | return $this; 48 | } 49 | 50 | public function returnValue(): self 51 | { 52 | $this->return_format = 'value'; 53 | return $this; 54 | } 55 | 56 | public function returnLabel(): self 57 | { 58 | $this->return_format = 'label'; 59 | return $this; 60 | } 61 | 62 | public function returnArray(): self 63 | { 64 | $this->return_format = 'array'; 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Fields/Acf/DateTimeField.php: -------------------------------------------------------------------------------- 1 | ID)); 22 | // Doing this will consider the time as UTC, it will print out just fine the date and time numbers, but as soon as you convert to timestamp or another timezone it will be totally off 23 | 24 | // Doing as below is better: 25 | // $start_date = new Carbon($start_date, 'America/Sao_Paulo'); 26 | // or with our helper: 27 | // $start_date = Date::carbon($start_date); 28 | 29 | public function displayFormat(string $format): self 30 | { 31 | $this->display_format = $format; 32 | return $this; 33 | } 34 | 35 | public function returnFormat(string $format): self 36 | { 37 | $this->return_format = $format; 38 | return $this; 39 | } 40 | 41 | public function weekStarts(int $weekday): self 42 | { 43 | $this->first_day = $weekday; 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/views/partials/feed-item.php: -------------------------------------------------------------------------------- 1 | 2 | <?= $this->title() ?> 3 | url() . $this->link() ?> 4 | url() . $this->link() ?> 5 | dateGmt()->toRssString() ?> 6 | terms() as $term) { 10 | echo 'name() . ']]>'; 11 | } 12 | 13 | // let's put together a basic content, an image and excerpt 14 | use Bond\Utils\Image; 15 | use Bond\Utils\Str; 16 | 17 | $content = ''; 18 | 19 | if ($image_id = $this->imageId()) { 20 | $content .= '

'; 21 | $content .= Image::imageTag($image_id, 'medium_large'); 22 | $content .= '

'; 23 | } 24 | if ($excerpt = $this->content()) { 25 | $result .= '

' . Str::clean($excerpt, 90) . '

'; 26 | } 27 | $content .= '

[' . tx('Link', 'rss', null, 'en') . ']

'; 28 | 29 | if (!empty($content)) { 30 | echo ''; 31 | echo '' . Str::clean($content, 40) . ''; 32 | } 33 | 34 | // example on how to add creator 35 | // echo 'author()?->name() . ']]>'; 36 | 37 | // I decided not to show the author by default as on many sites the WP editors are not actually the article author. 38 | // if needed just create a 'partials/feed-item.php' template on your theme and adjust according to your needs. 39 | ?> 40 |
41 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | view(); 23 | } 24 | } 25 | 26 | if (!function_exists('meta')) { 27 | function meta(): Meta 28 | { 29 | return app()->meta(); 30 | } 31 | } 32 | 33 | if (!function_exists('cache')) { 34 | function cache(): CacheInterface 35 | { 36 | return app()->cache(); 37 | } 38 | } 39 | 40 | if (!function_exists('c')) { 41 | function c(string $name, $default = null) 42 | { 43 | return defined($name) 44 | ? constant($name) 45 | : $default; 46 | } 47 | } 48 | 49 | if (!function_exists('t')) { 50 | function t( 51 | $input, 52 | string $language = null, 53 | string $written_language = null 54 | ) { 55 | return app()->translation() 56 | ->get($input, $language, $written_language); 57 | } 58 | } 59 | 60 | if (!function_exists('tx')) { 61 | function tx( 62 | $input, 63 | string $context, 64 | string $language = null, 65 | string $written_language = null 66 | ) { 67 | return app()->translation() 68 | ->get($input, $language, $written_language, $context); 69 | } 70 | } 71 | 72 | if (!function_exists('vite')) { 73 | function vite(): Vite 74 | { 75 | return new Vite(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Fields/Acf/TaxonomyField.php: -------------------------------------------------------------------------------- 1 | taxonomy = $taxonomy; 15 | return $this; 16 | } 17 | 18 | public function typeCheckbox(): self 19 | { 20 | $this->field_type = 'checkbox'; 21 | return $this; 22 | } 23 | 24 | public function typeRadio(): self 25 | { 26 | $this->field_type = 'radio'; 27 | return $this; 28 | } 29 | 30 | public function typeSelect(): self 31 | { 32 | $this->field_type = 'select'; 33 | return $this; 34 | } 35 | 36 | public function typeMultiSelect(): self 37 | { 38 | $this->field_type = 'multi_select'; 39 | return $this; 40 | } 41 | 42 | // should not be needed 43 | // public function allowNull(bool $active = true): self 44 | // { 45 | // $this->allow_null = $active; 46 | // return $this; 47 | // } 48 | 49 | public function loadTerms(bool $active = true): self 50 | { 51 | $this->load_terms = $active; 52 | return $this; 53 | } 54 | 55 | public function saveTerms(bool $active = true): self 56 | { 57 | $this->save_terms = $active; 58 | return $this; 59 | } 60 | 61 | public function allowNewTerms(bool $active = true): self 62 | { 63 | $this->add_term = $active; 64 | return $this; 65 | } 66 | 67 | public function returnId(): self 68 | { 69 | $this->return_format = 'id'; 70 | return $this; 71 | } 72 | 73 | public function returnObject(): self 74 | { 75 | $this->return_format = 'object'; 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Utils/File.php: -------------------------------------------------------------------------------- 1 | ID, 'original'); 17 | } 18 | 19 | public function caption(): string 20 | { 21 | return Image::caption($this->ID); 22 | } 23 | 24 | public function alt(): string 25 | { 26 | return Image::alt($this->ID); 27 | } 28 | 29 | public function meta(): ?array 30 | { 31 | return File::meta($this->ID); 32 | } 33 | 34 | public function pictureTag( 35 | $size = 'thumbnail', 36 | bool $with_caption = false 37 | ): string { 38 | return Image::pictureTag( 39 | $this->ID, 40 | $size, 41 | $with_caption 42 | ); 43 | } 44 | 45 | public function imageTag( 46 | $size = 'thumbnail', 47 | array $options = [] 48 | ): string { 49 | return Image::imageTag( 50 | $this->ID, 51 | $size, 52 | $options 53 | ); 54 | } 55 | 56 | public function imageUrl($size = 'thumbnail'): string 57 | { 58 | return Image::url($this->ID, $size); 59 | } 60 | 61 | public function values(string $for = ''): Fluent 62 | { 63 | $values = new Fluent(); 64 | 65 | switch ($for) { 66 | 67 | case 'archive': 68 | case 'search': 69 | $values->id = $this->ID; 70 | $values->imageTag = $this->pictureTag(); 71 | break; 72 | 73 | default: 74 | $values->id = $this->ID; 75 | $values->imageTag = $this->pictureTag($for); 76 | $values->caption = $this->caption(); 77 | break; 78 | } 79 | 80 | return $values; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /resources/assets/admin-color-scheme/scheme.scss: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // Custom Color Scheme 4 | // source from http://wordpress.org/plugins/admin-color-schemes/ 5 | 6 | // core variables 7 | $text-color: #666; 8 | $base-color: #fff; 9 | $icon-color: #666; 10 | $highlight-color: #000; 11 | 12 | // global 13 | // $body-background: #f1f1f1 !default; 14 | $link: #000; 15 | $link-focus: #999; 16 | $button-color: #000; 17 | $button-text-color: #fff; 18 | $form-checked: #333; 19 | 20 | // admin menu & admin-bar 21 | $menu-highlight-text: #fff; 22 | $menu-highlight-icon: #fff; 23 | $menu-highlight-background: #000; 24 | $menu-submenu-focus-text: #666; 25 | $menu-submenu-background: #f6f6f6; 26 | 27 | $menu-bubble-text: #fff; 28 | $menu-bubble-background: #000; 29 | $menu-bubble-current-text: #fff; 30 | $menu-bubble-current-background: #000; 31 | 32 | // 33 | // 34 | // Add the scheme 35 | @import "admin"; 36 | 37 | // 38 | // 39 | // More customizations 40 | .wrap .page-title-action { 41 | text-shadow: none !important; 42 | background-color: #fff; 43 | border-color: #aaa; 44 | color: #444 !important; 45 | box-shadow: none; 46 | 47 | &:hover, 48 | &:focus { 49 | box-shadow: none !important; 50 | background-color: #fafafa !important; 51 | border-color: #666; 52 | } 53 | } 54 | 55 | .view-switch a.current:before { 56 | color: #444; 57 | } 58 | 59 | input[type="text"]:focus, 60 | input[type="password"]:focus, 61 | input[type="color"]:focus, 62 | input[type="date"]:focus, 63 | input[type="datetime"]:focus, 64 | input[type="datetime-local"]:focus, 65 | input[type="email"]:focus, 66 | input[type="month"]:focus, 67 | input[type="number"]:focus, 68 | input[type="search"]:focus, 69 | input[type="tel"]:focus, 70 | input[type="text"]:focus, 71 | input[type="time"]:focus, 72 | input[type="url"]:focus, 73 | input[type="week"]:focus, 74 | input[type="checkbox"]:focus, 75 | input[type="radio"]:focus, 76 | select:focus, 77 | textarea:focus { 78 | border-color: #111; 79 | box-shadow: none; 80 | } 81 | -------------------------------------------------------------------------------- /src/Terms.php: -------------------------------------------------------------------------------- 1 | items = []; 14 | foreach ($terms as $term) { 15 | if ($term = Cast::term($term)) { 16 | $this->items[] = $term; 17 | } 18 | } 19 | return $this; 20 | } 21 | 22 | public function add($term, $index = -1): self 23 | { 24 | $term = Cast::term($term); 25 | if ($term) { 26 | array_splice($this->items, $index, 0, [$term]); 27 | } 28 | return $this; 29 | } 30 | 31 | public function addMany($terms, $index = -1): self 32 | { 33 | $all = []; 34 | foreach ($terms as $term) { 35 | if ($term = Cast::term($term)) { 36 | $all[] = $term; 37 | } 38 | } 39 | array_splice($this->items, $index, 0, $all); 40 | return $this; 41 | } 42 | 43 | public function unique(): self 44 | { 45 | $unique = []; 46 | 47 | foreach ($this->items as $item) { 48 | if (!isset($unique[$item->term_id])) { 49 | $unique[$item->term_id] = $item; 50 | } 51 | } 52 | 53 | $this->items = array_values($unique); 54 | return $this; 55 | } 56 | 57 | 58 | public function ids(): array 59 | { 60 | return array_column($this->items, 'term_id'); 61 | } 62 | 63 | public function names(string $language = null): array 64 | { 65 | $res = []; 66 | foreach ($this->items as $item) { 67 | $res[] = $item->name($language); 68 | } 69 | return $res; 70 | } 71 | 72 | public function slugs(string $language = null): array 73 | { 74 | $res = []; 75 | foreach ($this->items as $item) { 76 | $res[] = $item->slug($language); 77 | } 78 | return $res; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Fields/Acf/RelationshipField.php: -------------------------------------------------------------------------------- 1 | return_format = 'id'; 22 | return $this; 23 | } 24 | 25 | public function returnObject(): self 26 | { 27 | $this->return_format = 'object'; 28 | return $this; 29 | } 30 | 31 | public function postType($post_types): self 32 | { 33 | $this->post_type = (array) $post_types; 34 | return $this; 35 | } 36 | 37 | public function taxonomy($taxonomies): self 38 | { 39 | $this->taxonomy = (array) $taxonomies; 40 | return $this; 41 | } 42 | 43 | public function filters(array $filters): self 44 | { 45 | $this->filters = $filters; 46 | return $this; 47 | } 48 | 49 | public function removeFilters(): self 50 | { 51 | $this->filters = []; 52 | return $this; 53 | } 54 | 55 | public function removeSearchFilter(): self 56 | { 57 | $this->filters = array_diff($this->filters, ['search']); 58 | return $this; 59 | } 60 | 61 | public function removePostTypeFilter(): self 62 | { 63 | $this->filters = array_diff($this->filters, ['post_type']); 64 | return $this; 65 | } 66 | 67 | public function removeTaxonomyFilter(): self 68 | { 69 | $this->filters = array_diff($this->filters, ['taxonomy']); 70 | return $this; 71 | } 72 | 73 | public function elements(array $elements): self 74 | { 75 | $this->elements = $elements; 76 | return $this; 77 | } 78 | 79 | public function min(int $min): self 80 | { 81 | $this->min = $min; 82 | return $this; 83 | } 84 | 85 | public function max(int $max): self 86 | { 87 | $this->max = $max; 88 | return $this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Settings/Api.php: -------------------------------------------------------------------------------- 1 | 33 | \remove_action('wp_head', 'rest_output_link_wp_head', 10); 34 | \remove_action('xmlrpc_rsd_apis', 'rest_output_rsd'); 35 | } 36 | 37 | 38 | public static function disableDefaultRoutes() 39 | { 40 | // do not disable if is on admin 41 | if (app()->isAdmin()) { 42 | return; 43 | } 44 | 45 | // default routes 46 | \remove_action('rest_api_init', 'create_initial_rest_routes', 99); 47 | } 48 | 49 | 50 | public static function disableRootRoute() 51 | { 52 | // does not disable if is on admin 53 | if (app()->isAdmin()) { 54 | return; 55 | } 56 | 57 | // root endpoint 58 | \add_filter('rest_endpoints', function ($endpoints) { 59 | unset($endpoints['/']); 60 | return $endpoints; 61 | }); 62 | } 63 | 64 | 65 | public static function disableOembed() 66 | { 67 | // oembeds 68 | \remove_action('rest_api_init', 'wp_oembed_register_route'); 69 | } 70 | 71 | public static function onlyLoggedIn() 72 | { 73 | \add_filter('rest_authentication_errors', function ($access) { 74 | if (!\is_user_logged_in()) { 75 | return new WP_Error( 76 | 'rest_login_required', 77 | t('Not allowed'), 78 | ['status' => 401] 79 | ); 80 | } 81 | return $access; 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Posts.php: -------------------------------------------------------------------------------- 1 | items as $item) { 23 | if (!isset($unique[$item->ID])) { 24 | $unique[$item->ID] = $item; 25 | } 26 | } 27 | 28 | $this->items = array_values($unique); 29 | return $this; 30 | } 31 | 32 | public function ids(): array 33 | { 34 | return array_column($this->items, 'ID'); 35 | } 36 | 37 | public function titles(string $language = null): array 38 | { 39 | $res = []; 40 | foreach ($this->items as $item) { 41 | $res[] = $item->title($language); 42 | } 43 | return $res; 44 | } 45 | 46 | public function slugs(string $language = null): array 47 | { 48 | $res = []; 49 | foreach ($this->items as $item) { 50 | $res[] = $item->slug($language); 51 | } 52 | return $res; 53 | } 54 | 55 | public function except($posts): self 56 | { 57 | $to_remove = Cast::postsIds($posts); 58 | 59 | $this->items = array_filter( 60 | $this->items, 61 | function ($post) use ($to_remove) { 62 | return !in_array($post->ID, $to_remove); 63 | } 64 | ); 65 | return $this; 66 | } 67 | 68 | public function onlyOfTerms(int|iterable $terms): self 69 | { 70 | $ids = Cast::termsIds($terms); 71 | 72 | if (empty($ids)) { 73 | return $this; 74 | } 75 | 76 | $this->items = array_filter( 77 | $this->items, 78 | function ($post) use ($ids) { 79 | foreach ($post->termsIds() as $id) { 80 | if (in_array($id, $ids)) { 81 | return true; 82 | } 83 | } 84 | return false; 85 | } 86 | ); 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Fields/Acf/ImageField.php: -------------------------------------------------------------------------------- 1 | return_format = 'id'; 14 | return $this; 15 | } 16 | 17 | public function returnUrl(): self 18 | { 19 | $this->return_format = 'url'; 20 | return $this; 21 | } 22 | 23 | public function returnArray(): self 24 | { 25 | $this->return_format = 'array'; 26 | return $this; 27 | } 28 | 29 | public function previewSize(string $size): self 30 | { 31 | $this->preview_size = $size; 32 | return $this; 33 | } 34 | 35 | public function mimeTypes($extensions): self 36 | { 37 | if (is_array($extensions)) { 38 | $extensions = implode(',', $extensions); 39 | } 40 | $this->mime_types = (string) $extensions; 41 | return $this; 42 | } 43 | 44 | public function onlyJpg(): self 45 | { 46 | $this->mimeTypes('jpg,jpeg'); 47 | return $this; 48 | } 49 | 50 | public function libraryAll(): self 51 | { 52 | $this->library = 'all'; 53 | return $this; 54 | } 55 | 56 | public function libraryPost(): self 57 | { 58 | $this->library = 'uploadedTo'; 59 | return $this; 60 | } 61 | 62 | public function minWidth(int $width): self 63 | { 64 | $this->min_width = $width; 65 | return $this; 66 | } 67 | 68 | public function minHeight(int $height): self 69 | { 70 | $this->min_height = $height; 71 | return $this; 72 | } 73 | 74 | public function minSize(int $mb): self 75 | { 76 | $this->min_size = $mb; 77 | return $this; 78 | } 79 | 80 | public function maxWidth(int $width): self 81 | { 82 | $this->max_width = $width; 83 | return $this; 84 | } 85 | 86 | public function maxHeight(int $height): self 87 | { 88 | $this->max_height = $height; 89 | return $this; 90 | } 91 | 92 | public function maxSize(int $mb): self 93 | { 94 | $this->max_size = $mb; 95 | return $this; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/User.php: -------------------------------------------------------------------------------- 1 | init($values); 25 | } 26 | 27 | protected function init($values) 28 | { 29 | if (empty($values)) { 30 | return; 31 | } 32 | 33 | // if is numeric or string we'll fetch the WP_User and load fields 34 | // if is WP_User we'll just add it and load fields 35 | if ( 36 | is_numeric($values) 37 | || is_string($values) 38 | || $values instanceof WP_User 39 | ) { 40 | $user = Cast::wpUser($values); 41 | if ($user) { 42 | // unwrap the data 43 | $this->add($user->data); 44 | $user->data = null; 45 | 46 | // add the rest 47 | $this->add($user); 48 | 49 | // auto set the role 50 | if (!isset($this->role) && !empty($user->roles)) { 51 | $this->role = $user->roles[0]; 52 | } 53 | 54 | // load all fields 55 | $this->loadFields(); 56 | } 57 | return; 58 | } 59 | 60 | // otherwise (object or array) are honored as the full value 61 | // and added WITHOUT loading fields 62 | $this->add($values); 63 | } 64 | 65 | public function loadFields() 66 | { 67 | $this->add($this->getFields()); 68 | } 69 | 70 | public function getFields(): ?array 71 | { 72 | if (isset($this->ID) && app()->hasAcf()) { 73 | return \get_fields('user_' . $this->ID) ?: null; 74 | } 75 | return null; 76 | } 77 | 78 | public function link(string $language = null): string 79 | { 80 | return Link::forUsers($this, $language); 81 | } 82 | 83 | public function name(): string 84 | { 85 | return $this->display_name ?? ''; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /resources/assets/admin-color-scheme/_variables.scss: -------------------------------------------------------------------------------- 1 | // assign default value to all undefined variables 2 | 3 | 4 | // core variables 5 | 6 | $text-color: #fff !default; 7 | $base-color: #23282d !default; 8 | $icon-color: hsl( hue( $base-color ), 7%, 95% ) !default; 9 | $highlight-color: #0073aa !default; 10 | $notification-color: #d54e21 !default; 11 | 12 | 13 | // global 14 | 15 | $body-background: #f1f1f1 !default; 16 | 17 | $link: #0073aa !default; 18 | $link-focus: lighten( $link, 10% ) !default; 19 | 20 | $button-color: $highlight-color !default; 21 | $button-text-color: $text-color !default; 22 | 23 | $form-checked: #7e8993 !default; 24 | 25 | // admin menu & admin-bar 26 | 27 | $menu-text: $text-color !default; 28 | $menu-icon: $icon-color !default; 29 | $menu-background: $base-color !default; 30 | 31 | $menu-highlight-text: $text-color !default; 32 | $menu-highlight-icon: $text-color !default; 33 | $menu-highlight-background: $highlight-color !default; 34 | 35 | $menu-current-text: $menu-highlight-text !default; 36 | $menu-current-icon: $menu-highlight-icon !default; 37 | $menu-current-background: $menu-highlight-background !default; 38 | 39 | $menu-submenu-text: mix( $base-color, $text-color, 30% ) !default; 40 | $menu-submenu-background: darken( $base-color, 7% ) !default; 41 | $menu-submenu-background-alt: desaturate( lighten( $menu-background, 7% ), 7% ) !default; 42 | 43 | $menu-submenu-focus-text: $highlight-color !default; 44 | $menu-submenu-current-text: $text-color !default; 45 | 46 | $menu-bubble-text: $text-color !default; 47 | $menu-bubble-background: $notification-color !default; 48 | $menu-bubble-current-text: $text-color !default; 49 | $menu-bubble-current-background: $menu-submenu-background !default; 50 | 51 | $menu-collapse-text: $menu-icon !default; 52 | $menu-collapse-icon: $menu-icon !default; 53 | $menu-collapse-focus-text: $text-color !default; 54 | $menu-collapse-focus-icon: $menu-highlight-icon !default; 55 | 56 | $adminbar-avatar-frame: lighten( $menu-background, 7% ) !default; 57 | $adminbar-input-background: lighten( $menu-background, 7% ) !default; 58 | 59 | $adminbar-recovery-exit-text: $menu-bubble-text !default; 60 | $adminbar-recovery-exit-background: $menu-bubble-background !default; 61 | $adminbar-recovery-exit-background-alt: mix(black, $adminbar-recovery-exit-background, 10%) !default; 62 | 63 | $menu-customizer-text: mix( $base-color, $text-color, 40% ) !default; 64 | -------------------------------------------------------------------------------- /resources/views/templates/feed.php: -------------------------------------------------------------------------------- 1 | ' . PHP_EOL; 9 | 10 | ?> 11 | 12 | 13 | <?= $this->title ?> 14 | 15 | url() ?> 16 | description ?> 17 | last_build_date ?> 18 | language ?> 19 | update_period ?> 20 | update_frequency ?> 21 | copyright ?> 22 | 23 | image) : ?> 24 | 25 | image ?> 26 | <?= $this->title ?> 27 | url() ?> 28 | 29 | partial('feed-item-' . $p->post_type, $p) 42 | ?: $this->partial('feed-item', $p); 43 | 44 | // templating will look in this order: 45 | // partials/feed-item-{post_type}-{feed_key} (at theme view folder) 46 | // partials/feed-item-{post_type}-{feed_key} (at bond source) 47 | // partials/feed-item-{post_type} (at theme view folder) 48 | // partials/feed-item-{post_type} (at bond source) 49 | // partials/feed-item-{feed_key} (at theme view folder) 50 | // partials/feed-item-{feed_key} (at bond source) 51 | // partials/feed-item (at theme view folder) 52 | // partials/feed-item (at bond source) 53 | } 54 | ?> 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Services/SitemapRenderer.php: -------------------------------------------------------------------------------- 1 | ', 15 | $this->stylesheet, 16 | '' 17 | ) 18 | ); 19 | 20 | foreach ($url_list as $url_item) { 21 | $url = $urlset->addChild('url'); 22 | 23 | // Add each element as a child node to the entry. 24 | foreach ($url_item as $name => $value) { 25 | if ('loc' === $name) { 26 | $url->addChild($name, esc_url($value)); 27 | } elseif (in_array($name, array('lastmod', 'changefreq', 'priority'), true)) { 28 | $url->addChild($name, esc_xml($value)); 29 | /* [end] Unmodified from WP core */ 30 | // 31 | } elseif ('alternates' === $name) { 32 | if (!empty($value)) { 33 | foreach ($value as $lang => $link) { 34 | $alternates = $url->addChild('xhtml:link'); 35 | $alternates->addAttribute('rel', 'alternate'); 36 | $alternates->addAttribute('hreflang', $lang); 37 | $alternates->addAttribute('href', esc_url($link)); 38 | } 39 | } 40 | // 41 | /* [start] Unmodified from WP core */ 42 | } else { 43 | _doing_it_wrong( 44 | __METHOD__, 45 | sprintf( 46 | /* translators: %s: List of element names. */ 47 | __('Fields other than %s are not currently supported for sitemaps.'), 48 | implode(',', array('loc', 'lastmod', 'changefreq', 'priority')) 49 | ), 50 | '5.5.0' 51 | ); 52 | } 53 | } 54 | } 55 | 56 | return $urlset->asXML(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Taxonomy.php: -------------------------------------------------------------------------------- 1 | static::$name, 45 | 'singular_name' => static::$singular_name, 46 | ], static::$register_options) 47 | ); 48 | } 49 | 50 | 51 | // helpers 52 | 53 | public static function fieldGroup(string $title): FieldGroup 54 | { 55 | return (new FieldGroup(static::$taxonomy)) 56 | ->title($title) 57 | ->location([ 58 | 'tax' => static::$taxonomy 59 | ]); 60 | } 61 | 62 | public static function name(bool $singular = false): string 63 | { 64 | return $singular 65 | ? static::$singular_name ?? Query::taxonomyName(static::$taxonomy, true) 66 | : static::$name ?? Query::taxonomyName(static::$taxonomy); 67 | } 68 | 69 | // TODO 70 | // public static function count() 71 | 72 | public static function all(array $params = []): Terms 73 | { 74 | return Query::allTerms(static::$taxonomy, $params); 75 | } 76 | 77 | 78 | protected static function setColumns(array $columns) 79 | { 80 | app()->adminColumns()->setTaxonomyColumns(static::$taxonomy, $columns); 81 | } 82 | 83 | protected static function addColumnHandler( 84 | string $name, 85 | callable $handler = null, 86 | string|int $width = 0, 87 | string $css = null 88 | ) { 89 | app()->adminColumns()->addHandler($name, $handler, $width, $css); 90 | } 91 | 92 | protected static function setDefaultFields(array $fields) 93 | { 94 | Admin::setTaxonomyFields(static::$taxonomy, $fields); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Utils/Date.php: -------------------------------------------------------------------------------- 1 | timestamp : 0; 18 | } 19 | 20 | public static function year($date = true): int 21 | { 22 | return (int) static::iso($date, 'Y'); 23 | } 24 | 25 | 26 | public static function wp( 27 | $date = true, 28 | string $from_timezone = null, 29 | string $to_timezone = null 30 | ): string { 31 | 32 | $date = static::carbon($date, $from_timezone); 33 | 34 | if ($date) { 35 | if ($from_timezone && !$to_timezone) { 36 | $to_timezone = app()->timezone(); 37 | } 38 | if ($to_timezone) { 39 | $date->setTimezone($to_timezone); 40 | } 41 | return $date->isoFormat('Y-MM-DD HH:mm:ss'); 42 | } 43 | return ''; 44 | } 45 | 46 | 47 | 48 | 49 | public static function iso( 50 | $date = true, 51 | $format = 'Y-MM-DD HH:mm:ss' 52 | ): string { 53 | $date = static::carbon($date); 54 | return $date ? $date->isoFormat($format) : ''; 55 | } 56 | 57 | 58 | public static function carbon( 59 | $date = true, 60 | string $timezone = null 61 | ): ?Carbon { 62 | if (empty($date)) { 63 | return null; 64 | } 65 | if ($date instanceof Carbon) { 66 | return $date; 67 | } 68 | 69 | // MongoDB date 70 | if (is_a($date, 'MongoDB\BSON\UTCDateTime')) { 71 | $time = (int) ((string) $date); 72 | $time = round($time / 1000); 73 | return Carbon::createFromTimestampUTC($time); 74 | } 75 | if (!empty($date['milliseconds'])) { 76 | $time = (int) $date['milliseconds']; 77 | $time = round($time / 1000); 78 | return Carbon::createFromTimestampUTC($time); 79 | } 80 | 81 | return new Carbon( 82 | $date === true ? null : $date, 83 | $timezone ?: app()->timezone() 84 | ); 85 | } 86 | 87 | 88 | public static function header( 89 | $date = true, 90 | string $timezone = null 91 | ): string { 92 | $date = static::carbon($date, $timezone); 93 | return $date ? $date->toRfc7231String() : ''; 94 | } 95 | 96 | 97 | public static function mongoDb( 98 | $date = true, 99 | string $timezone = null 100 | ): ?\MongoDB\BSON\UTCDateTime { 101 | 102 | if ($date instanceof \MongoDB\BSON\UTCDateTime) { 103 | return $date; 104 | } 105 | 106 | $date = static::carbon($date, $timezone); 107 | 108 | return $date ? new \MongoDB\BSON\UTCDateTime($date) : null; 109 | } 110 | 111 | // determine a ttl in seconds 112 | public static function ttl(int|string|DateInterval|DateTimeInterface $ttl): int 113 | { 114 | if (is_numeric($ttl)) { 115 | return (int) $ttl; 116 | } 117 | 118 | if (is_string($ttl)) { 119 | $ttl = static::carbon($ttl); 120 | } 121 | 122 | if ($ttl instanceof DateInterval) { 123 | $ttl = static::carbon()->add($ttl); 124 | } 125 | 126 | if ($ttl instanceof DateTimeInterface) { 127 | $ttl = static::carbon()->diffInRealSeconds($ttl, false); 128 | } 129 | 130 | $ttl = (int) $ttl; 131 | if ($ttl < 0) { 132 | $ttl = 0; 133 | } 134 | return $ttl; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Term.php: -------------------------------------------------------------------------------- 1 | isEnabled()) { 23 | $this->initFromCache($values); 24 | } else { 25 | $this->init($values); 26 | } 27 | } 28 | 29 | protected function initFromCache($values) 30 | { 31 | if ($id = Cast::termId($values)) { 32 | 33 | $has_initted = false; 34 | 35 | $cached = $this->add(cache()->remember( 36 | 'bond/terms/' . $id, 37 | function () use ($values, &$has_initted) { 38 | $this->init($values); 39 | $has_initted = true; 40 | return $this->toArray(); 41 | } 42 | )); 43 | if (!$has_initted) { 44 | $this->add($cached); 45 | } 46 | } else { 47 | $this->init($values); 48 | } 49 | } 50 | 51 | protected function init($values) 52 | { 53 | if (empty($values)) { 54 | return; 55 | } 56 | 57 | // if is numeric we'll fetch the WP_Term and load fields 58 | // if is WP_Term we'll just add it and load fields 59 | if (is_numeric($values) || $values instanceof WP_Term) { 60 | $term = Cast::wpTerm($values); 61 | if ($term) { 62 | $this->add($term); 63 | $this->loadFields(); 64 | } 65 | return; 66 | } 67 | 68 | // if is string we'll try to find by slug and load fields 69 | if (is_string($values)) { 70 | if (isset($this->taxonomy)) { 71 | $term = Query::wpTermBySlug( 72 | $values, 73 | $this->taxonomy 74 | ); 75 | if ($term) { 76 | $this->add($term); 77 | $this->loadFields(); 78 | } 79 | } 80 | return; 81 | } 82 | 83 | // otherwise (object or array) are honored as the full value to added WITHOUT loading fields 84 | $this->add($values); 85 | } 86 | 87 | public function loadFields() 88 | { 89 | $this->add($this->getFields()); 90 | } 91 | 92 | public function getFields(): ?array 93 | { 94 | if ( 95 | isset($this->term_id) 96 | && isset($this->taxonomy) 97 | && app()->hasAcf() 98 | ) { 99 | return \get_fields($this->taxonomy . '_' . $this->term_id) ?: null; 100 | } 101 | return null; 102 | } 103 | 104 | public function isMultilanguage(): bool 105 | { 106 | return app()->multilanguage()->is($this); 107 | } 108 | 109 | public function taxonomyName( 110 | bool $singular = false, 111 | string $language = null 112 | ): string { 113 | return Query::taxonomyName($this->taxonomy, $singular, $language); 114 | } 115 | 116 | public function name(string $language = null): string 117 | { 118 | if ($this->isMultilanguage()) { 119 | return $this->get('name', $language) ?: $this->name; 120 | } 121 | return $this->name; 122 | } 123 | 124 | public function slug(string $language = null): string 125 | { 126 | if (!isset($this->slug)) { 127 | return ''; 128 | } 129 | if ($this->isMultilanguage()) { 130 | return $this->get('slug', $language) ?: $this->slug; 131 | } 132 | return $this->slug; 133 | } 134 | 135 | public function link(string $language = null): string 136 | { 137 | return Link::forTerms($this, $language); 138 | } 139 | 140 | public function isEmpty(): bool 141 | { 142 | return empty($this->term_id) 143 | || empty($this->taxonomy) 144 | || parent::isEmpty(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Utils/Arr.php: -------------------------------------------------------------------------------- 1 | get_params(); 28 | } 29 | if (is_object($value)) { 30 | if (method_exists($value, 'toArray')) { 31 | return $value->toArray(); 32 | } 33 | if (method_exists($value, 'getArrayCopy')) { 34 | return $value->getArrayCopy(); 35 | } 36 | return get_object_vars($value); 37 | } 38 | return (array) $value; 39 | } 40 | 41 | public static function arrayRecursive($object, string $only = null): array 42 | { 43 | if (is_null($object)) { 44 | return []; 45 | } 46 | if (!is_object($object) && !is_array($object)) { 47 | return []; 48 | } 49 | 50 | $result = []; 51 | foreach ($object as $key => $value) { 52 | 53 | if (is_array($value)) { 54 | $result[$key] = static::arrayRecursive($value, $only); 55 | } elseif ($only) { 56 | if (is_a($value, $only)) { 57 | $result[$key] = static::arrayRecursive($value, $only); 58 | } else { 59 | $result[$key] = $value; 60 | } 61 | } elseif (is_object($value)) { 62 | if (method_exists($value, 'toArray')) { 63 | $result[$key] = static::arrayRecursive($value->toArray(), $only); 64 | } elseif (method_exists($value, 'getArrayCopy')) { 65 | $result[$key] = static::arrayRecursive($value->getArrayCopy(), $only); 66 | } else { 67 | $result[$key] = static::arrayRecursive(get_object_vars($value), $only); 68 | } 69 | } else { 70 | $result[$key] = $value; 71 | } 72 | } 73 | return $result; 74 | } 75 | 76 | public static function mapKeys(callable $f, array $array): array 77 | { 78 | $result = []; 79 | foreach ($array as $key => $value) { 80 | $k = $f($key); 81 | 82 | if (is_array($value)) { 83 | $result[$k] = static::mapKeys($f, $value); 84 | } elseif ($value instanceof Fluent || $value instanceof FluentList) { 85 | $result[$k] = $value->mapKeys($f); 86 | } else { 87 | $result[$k] = $value; 88 | } 89 | } 90 | return $result; 91 | } 92 | 93 | public static function camelKeys(array $array): array 94 | { 95 | return static::mapKeys([Str::class, 'camel'], $array); 96 | } 97 | 98 | public static function snakeKeys(array $array): array 99 | { 100 | return static::mapKeys([Str::class, 'snake'], $array); 101 | } 102 | 103 | /** 104 | * Determine whether the given value is array accessible. 105 | * 106 | * @param mixed $value 107 | * @return bool 108 | */ 109 | public static function accessible($value): bool 110 | { 111 | return is_array($value) || $value instanceof ArrayAccess; 112 | } 113 | 114 | // $keys array|string 115 | public static function only(array $values, array $keys): array 116 | { 117 | return array_intersect_key($values, array_flip((array) $keys)); 118 | } 119 | 120 | public static function except(array $values, array $keys): array 121 | { 122 | return array_diff_key($values, array_flip((array) $keys)); 123 | } 124 | 125 | public static function sortKeys( 126 | array $array, 127 | int $flags = SORT_NATURAL | SORT_FLAG_CASE 128 | ): array { 129 | 130 | ksort($array, $flags); 131 | 132 | foreach ($array as &$value) { 133 | if (is_array($value) && $value) { 134 | $value = static::sortKeys($value, $flags); 135 | } 136 | } 137 | 138 | return $array; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Utils/Filesystem.php: -------------------------------------------------------------------------------- 1 | 0; 55 | } 56 | 57 | /** 58 | * Write the contents of a file, replacing it atomically if it already exists. 59 | */ 60 | public static function replace(string $path, string $content) 61 | { 62 | // code from © Laravel LLC / Taylor Otwell, MIT licence. 63 | 64 | // If the path already exists and is a symlink, get the real path... 65 | clearstatcache(true, $path); 66 | 67 | $path = realpath($path) ?: $path; 68 | 69 | $tempPath = tempnam(dirname($path), basename($path)); 70 | 71 | // Fix permissions of tempPath because `tempnam()` creates it with permissions set to 0600... 72 | chmod($tempPath, 0777 - umask()); 73 | 74 | file_put_contents($tempPath, $content); 75 | 76 | rename($tempPath, $path); 77 | } 78 | 79 | public static function delete(string $path): bool 80 | { 81 | if (is_dir($path)) { 82 | $items = new FilesystemIterator($path); 83 | 84 | foreach ($items as $item) { 85 | 86 | // skip hidden 87 | // if (strpos($item->getFilename(), '.') === 0) { 88 | // continue; 89 | // } 90 | 91 | // recurse if found another directory 92 | // delete if found a file 93 | if ($item->isDir() && !$item->isLink()) { 94 | static::delete($item->getPathname()); 95 | // @rmdir($item->getPathname()); 96 | } else { 97 | @unlink($item->getPathname()); 98 | } 99 | } 100 | unset($items); 101 | 102 | // remove the directory itself 103 | // @rmdir($path); 104 | // TODO it is erroring out, shoudld be a race condition 105 | // see https://stackoverflow.com/questions/11513488/php-mkdir-not-working-after-rmdir 106 | 107 | // also should be an error with ::put LOCK_EX 108 | 109 | } else { 110 | @unlink($path); 111 | } 112 | 113 | return true; 114 | 115 | // TODO see this, might just be the best solution!!! 116 | // https://github.com/spatie/temporary-directory/blob/main/src/TemporaryDirectory.php#L147 117 | 118 | // if (is_link($path)) { 119 | // return unlink($path); 120 | // } 121 | 122 | // if (! file_exists($path)) { 123 | // return true; 124 | // } 125 | 126 | // if (! is_dir($path)) { 127 | // return unlink($path); 128 | // } 129 | 130 | // foreach (new FilesystemIterator($path) as $item) { 131 | // if (! static::delete($item)) { 132 | // return false; 133 | // } 134 | // } 135 | 136 | // /* 137 | // * By forcing a php garbage collection cycle using gc_collect_cycles() we can ensure 138 | // * that the rmdir does not fail due to files still being reserved in memory. 139 | // */ 140 | // gc_collect_cycles(); 141 | 142 | // return rmdir($path); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Utils/Register.php: -------------------------------------------------------------------------------- 1 | true, 14 | 'menu_position' => null, 15 | 'menu_icon' => '', 16 | 'supports' => [ 17 | 'title', 18 | // 'editor', 19 | // 'author', 20 | // 'thumbnail', 21 | // 'excerpt', 22 | // 'custom-fields', 23 | 'revisions', 24 | // 'page-attributes', 25 | // 'post-formats', 26 | ], 27 | 'taxonomies' => [], 28 | 'has_archive' => true, 29 | // 'publicly_queryable' => true, 30 | 'query_var' => true, 31 | 'rewrite' => false, 32 | ]; 33 | $params = array_merge($base_args, $params); 34 | 35 | // auto labels 36 | if (!isset($params['labels']) && isset($params['name']) && isset($params['singular_name'])) { 37 | 38 | $x = 'register'; 39 | 40 | $name = tx($params['name'], $x); 41 | $singular_name = tx($params['singular_name'], $x); 42 | 43 | // translate to english to append to labels 44 | $n = Str::lower(tx($params['singular_name'], $x, 'en')); 45 | 46 | $params['labels'] = [ 47 | 'name' => $name, 48 | 'singular_name' => $singular_name, 49 | 'add_new' => tx('Add ' . $n, $x, null, 'en'), 50 | 'add_new_item' => tx('Add new ' . $n, $x, null, 'en'), 51 | 'edit_item' => tx('Edit ' . $n, $x, null, 'en'), 52 | 'new_item' => tx('New ' . $n, $x, null, 'en'), 53 | 'view_item' => tx('View ' . $n, $x, null, 'en'), 54 | 'search_items' => tx('Search ' . $n, $x, null, 'en'), 55 | 'not_found' => tx('No ' . $n . ' found', $x, null, 'en'), 56 | 'not_found_in_trash' => tx('No ' . $n . ' found in trash', $x, null, 'en'), 57 | ]; 58 | } 59 | 60 | \add_action('init', function () use ($post_type, $params) { 61 | \register_post_type($post_type, $params); 62 | }, 9); 63 | } 64 | 65 | 66 | 67 | // Tip: post_tag and category can be overriden, just register again 68 | public static function taxonomy($taxonomy, array $params = []) 69 | { 70 | $base_args = [ 71 | 'public' => true, 72 | 'hierarchical' => true, 73 | 'show_admin_column' => true, 74 | 'show_tagcloud' => false, 75 | 'show_in_nav_menus' => false, 76 | 'show_ui' => true, 77 | 78 | 'rewrite' => false, 79 | 'query_var' => true, 80 | 'meta_box_cb' => false, 81 | ]; 82 | $params = array_merge($base_args, $params); 83 | 84 | // auto labels 85 | if (!isset($params['labels']) && isset($params['name']) && isset($params['singular_name'])) { 86 | 87 | $x = 'register'; 88 | 89 | $name = tx($params['name'], $x); 90 | $singular_name = tx($params['singular_name'], $x); 91 | 92 | // translate to english to append to labels 93 | $n = Str::lower(tx($params['name'], $x, 'en')); 94 | $ns = Str::lower(tx($params['singular_name'], $x, 'en')); 95 | 96 | $params['labels'] = [ 97 | 'name' => $name, 98 | 'singular_name' => $singular_name, 99 | 'all_items' => tx('All ' . $n, $x, null, 'en'), 100 | 'add_new_item' => tx('Add new ' . $ns, $x, null, 'en'), 101 | 'new_item_name' => tx('New ' . $ns, $x, null, 'en'), 102 | 'edit_item' => tx('Edit ' . $ns, $x, null, 'en'), 103 | 'new_item' => tx('New ' . $ns, $x, null, 'en'), 104 | 'view_item' => tx('View ' . $ns, $x, null, 'en'), 105 | 'update_item' => tx('Update ' . $ns, $x, null, 'en'), 106 | 'search_items' => tx('Search ' . $n, $x, null, 'en'), 107 | 'not_found' => tx('No ' . $ns . ' found', $x, null, 'en'), 108 | 'not_found_in_trash' => tx('No ' . $ns . ' found in trash', $x, null, 'en'), 109 | 'parent_item' => tx('Parent ' . $ns, $x, null, 'en'), 110 | 'parent_item_colon' => tx('Parent ' . $ns, $x) . ':', 111 | 'add_or_remove_items' => tx('Add or remove ' . $n, $x, null, 'en'), 112 | 'choose_from_most_used' => tx('Choose from the most used ' . $n, $x, null, 'en'), 113 | ]; 114 | } 115 | 116 | \add_action('init', function () use ($taxonomy, $params) { 117 | \register_taxonomy($taxonomy, null, $params); 118 | }, 9); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Utils/Obj.php: -------------------------------------------------------------------------------- 1 | $value) { 41 | 42 | // recurse 43 | if (is_array($value)) { 44 | $target[$key] = self::localizeProperties($value, [], $code); 45 | continue; 46 | } 47 | if ( 48 | $value instanceof Fluent 49 | || $value instanceof FluentList 50 | ) { 51 | $target[$key] = $value->localized(); // TODO should bring the create_new parameter? 52 | continue; 53 | } 54 | 55 | // localize 56 | foreach (Language::codes() as $c) { 57 | $suffix = Language::fieldsSuffix($c); 58 | 59 | if (str_ends_with($key, $suffix)) { 60 | 61 | // if current, store it 62 | if ($c === $code) { 63 | $unlocalized_key = substr($key, 0, -strlen($suffix)); 64 | $target[$unlocalized_key] = $value; 65 | } 66 | // else just skip 67 | continue 2; 68 | } 69 | } 70 | 71 | // just add non localized values 72 | // if not already added by the localization above 73 | if (!isset($target[$key])) { 74 | $target[$key] = $value; 75 | } 76 | } 77 | 78 | return $target; 79 | } 80 | 81 | /** 82 | * Gets an object public properties out into an Array. 83 | * Does not do it recursivelly. 84 | */ 85 | public static function vars($object, bool $skip_null = false): array 86 | { 87 | if (is_null($object)) { 88 | return []; 89 | } 90 | 91 | $values = is_array($object) 92 | ? $object 93 | : get_object_vars($object); 94 | 95 | if ($skip_null) { 96 | $filtered = []; 97 | foreach ($values as $key => $value) { 98 | if ($value === null) { 99 | continue; 100 | } 101 | $filtered[$key] = $value; 102 | } 103 | return $filtered; 104 | } 105 | 106 | return $values; 107 | } 108 | 109 | 110 | /** 111 | * Converts any object/class found to Array. It does it recursivelly. 112 | * 113 | * Executes the toArray / getArrayCopy method if found, otherwise will get all public properties. 114 | */ 115 | public static function toArray($object, bool $skip_null = false): array 116 | { 117 | $result = []; 118 | 119 | foreach ($object as $key => $value) { 120 | 121 | if ($skip_null && $value === null) { 122 | continue; 123 | } 124 | 125 | if (is_array($value) || is_a($value, 'stdClass')) { 126 | // recurse directly 127 | $result[$key] = static::toArray($value, $skip_null); 128 | // 129 | } elseif (is_object($value)) { 130 | 131 | if (method_exists($value, 'toArray')) { 132 | $result[$key] = static::toArray($value->toArray(), $skip_null); 133 | // 134 | } elseif (method_exists($value, 'getArrayCopy')) { 135 | $result[$key] = static::toArray($value->getArrayCopy(), $skip_null); 136 | // 137 | } else { 138 | // extract public properties and recurse 139 | $result[$key] = static::toArray(get_object_vars($value), $skip_null); 140 | } 141 | } else { 142 | $result[$key] = $value; 143 | } 144 | } 145 | 146 | return $result; 147 | } 148 | 149 | 150 | /** 151 | * Recursive clone. 152 | * 153 | * @see https://bugs.php.net/bug.php?id=49664 154 | * @param mixed $object 155 | * @return mixed 156 | * @throws ReflectionException 157 | */ 158 | public static function clone($object) 159 | { 160 | if (is_array($object)) { 161 | foreach ($object as $key => $value) { 162 | $object[$key] = static::clone($value); 163 | } 164 | return $object; 165 | } 166 | 167 | if (!is_object($object)) { 168 | return $object; 169 | } 170 | 171 | if (!(new ReflectionClass($object))->isCloneable()) { 172 | return $object; 173 | } 174 | 175 | return clone $object; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Utils/Acf.php: -------------------------------------------------------------------------------- 1 | $v) { 22 | 23 | if (is_array($v)) { 24 | 25 | $values[$k] = static::unwrapControlField( 26 | isset($v['type']) && isset($v['value']) ? $v['value'] : $v 27 | ); 28 | } else { 29 | $values[$k] = $v; 30 | } 31 | } 32 | return $values; 33 | } 34 | 35 | return $field; 36 | } 37 | 38 | public static function getControlFields($post_id): array 39 | { 40 | // get all post fields without formatting 41 | $acf = \get_field_objects($post_id, false); 42 | if (empty($acf)) { 43 | return []; 44 | } 45 | 46 | // assemble the control field list 47 | $fields = []; 48 | foreach ($acf as $name => $values) { 49 | $fields[$name] = [ 50 | 'type' => $values['type'], 51 | 'value' => static::acfFieldValue($values), 52 | ]; 53 | } 54 | 55 | return $fields; 56 | } 57 | 58 | 59 | public static function acfFieldValue($values) 60 | { 61 | if (in_array($values['type'], [ 62 | 'flexible_content', 63 | 'repeater', 64 | ])) { 65 | 66 | // put together all sub fields 67 | if ($values['type'] === 'repeater') { 68 | $values['layouts'] = [ 69 | ['sub_fields' => $values['sub_fields']] 70 | ]; 71 | } 72 | $lookup = []; 73 | foreach ($values['layouts'] as $layout) { 74 | foreach ($layout['sub_fields'] as $sub) { 75 | $lookup[$sub['key']] = [ 76 | 'name' => $sub['name'], 77 | 'type' => $sub['type'], 78 | 'layouts' => $sub['layouts'] ?? null, 79 | 'sub_fields' => $sub['sub_fields'] ?? null, 80 | ]; 81 | } 82 | } 83 | 84 | // format all sub values 85 | $value = []; 86 | if (!empty($values['value'])) { 87 | foreach ($values['value'] as $layout) { 88 | $layout_value = []; 89 | 90 | foreach ($layout as $k => $v) { 91 | 92 | if ($k === 'acf_fc_layout') { 93 | $layout_value[$k] = [ 94 | 'type' => '', 95 | 'value' => $v, 96 | ]; 97 | } else { 98 | $field = $lookup[$k]; 99 | $field['value'] = $v; 100 | 101 | $layout_value[$field['name']] = [ 102 | 'type' => $field['type'], 103 | 'value' => static::acfFieldValue($field), 104 | ]; 105 | } 106 | } 107 | 108 | $value[] = $layout_value; 109 | } 110 | } 111 | return $value; 112 | } 113 | 114 | if ($values['type'] === 'group') { 115 | 116 | // put together all sub fields 117 | $lookup = []; 118 | foreach ($values['sub_fields'] as $sub) { 119 | $lookup[$sub['key']] = [ 120 | 'name' => $sub['name'], 121 | 'type' => $sub['type'], 122 | 'layouts' => $sub['layouts'] ?? null, 123 | 'sub_fields' => $sub['sub_fields'] ?? null, 124 | ]; 125 | } 126 | 127 | // format all sub values 128 | $value = []; 129 | if (!empty($values['value'])) { 130 | foreach ($values['value'] as $k => $v) { 131 | 132 | $field = $lookup[$k]; 133 | $field['value'] = $v; 134 | 135 | $value[$field['name']] = [ 136 | 'type' => $field['type'], 137 | 'value' => static::acfFieldValue($field), 138 | ]; 139 | } 140 | } 141 | 142 | return $value; 143 | } 144 | 145 | switch ($values['type']) { 146 | case 'gallery': 147 | case 'relationship': 148 | return array_map('intval', empty($values['value']) ? [] : (array)$values['value']); 149 | 150 | case 'true_false': 151 | return (bool) $values['value']; 152 | 153 | case 'number': 154 | return (float) $values['value']; 155 | 156 | case 'image': 157 | case 'file': 158 | return (int) $values['value']; 159 | 160 | case 'post_object': 161 | return is_array($values['value']) 162 | ? array_map('intval', $values['value']) 163 | : (int) $values['value']; 164 | 165 | // TODO more? 166 | 167 | default; 168 | break; 169 | } 170 | 171 | 172 | // for all other field types, just return the value 173 | return $values['value']; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/PostType.php: -------------------------------------------------------------------------------- 1 | postType('products')->fieldGroup(...); 21 | 22 | 23 | abstract class PostType 24 | { 25 | public static string $post_type; 26 | public static array $taxonomies = []; 27 | public static string $name; 28 | public static string $singular_name; 29 | public static array $register_options = []; 30 | 31 | // we may elect some more props here 32 | 33 | 34 | public static function link(string $language = null): string 35 | { 36 | return Link::forPostTypes(static::$post_type, $language); 37 | } 38 | 39 | public static function register() 40 | { 41 | if (!isset(static::$post_type)) { 42 | return; 43 | } 44 | 45 | // these are already registered by WP 46 | if (in_array(static::$post_type, [ 47 | 'post', 48 | 'page', 49 | 'attachment', 50 | ])) { 51 | return; 52 | } 53 | 54 | // auto set names 55 | // we won't handle plural, but at least helps 56 | if (!isset(static::$name)) { 57 | static::$name = Str::title((static::$post_type), true); 58 | } 59 | if (!isset(static::$singular_name)) { 60 | static::$singular_name = Str::title((static::$post_type), true); 61 | } 62 | 63 | // register post type 64 | Register::postType( 65 | static::$post_type, 66 | array_merge([ 67 | 'name' => static::$name, 68 | 'singular_name' => static::$singular_name, 69 | 'taxonomies' => static::$taxonomies, 70 | ], static::$register_options) 71 | ); 72 | } 73 | 74 | public static function addToView() 75 | { 76 | if (static::$post_type === 'page') { 77 | \add_action( 78 | 'Bond/ready/page', 79 | [static::class, 'single'] 80 | ); 81 | } else { 82 | \add_action( 83 | 'Bond/ready/archive-' . static::$post_type, 84 | [static::class, 'archive'] 85 | ); 86 | \add_action( 87 | 'Bond/ready/single-' . static::$post_type, 88 | [static::class, 'single'] 89 | ); 90 | } 91 | } 92 | 93 | // TODO IDEA, we could automatically provide directly in View 94 | // just the posts actually, since we don't need to rely on global $post / $posts; 95 | // MAYBE even the post itself is sent to the view, just like the RSS item 96 | public static function archive() 97 | { 98 | global $posts; 99 | view()->items = Cast::posts($posts)->values('archive'); 100 | } 101 | 102 | public static function single() 103 | { 104 | global $post; 105 | 106 | if ($p = Cast::post($post)) { 107 | view()->add($p->values('single')); 108 | } 109 | } 110 | 111 | // helpers 112 | 113 | public static function fieldGroup(string $title): FieldGroup 114 | { 115 | return (new FieldGroup(static::$post_type)) 116 | ->title($title) 117 | ->location(static::$post_type); 118 | } 119 | 120 | public static function name(bool $singular = false): string 121 | { 122 | return Query::postTypeName(static::$post_type, $singular); 123 | } 124 | 125 | public static function count(): int 126 | { 127 | return cache()->remember( 128 | static::$post_type . '/count', 129 | function () { 130 | return Query::count(static::$post_type); 131 | }, 132 | -1 133 | ); 134 | } 135 | 136 | public static function all(array $params = []): Posts 137 | { 138 | return Query::all(static::$post_type, $params); 139 | } 140 | 141 | public static function allOfTerms($terms, array $params = []): Posts 142 | { 143 | return self::all(array_merge( 144 | $params, 145 | [ 146 | 'tax_query' => Query::formatTaxQuery($terms), 147 | ] 148 | )); 149 | } 150 | 151 | public static function terms( 152 | string|array $taxonomy, 153 | array $params = [] 154 | ): Terms { 155 | return Query::termsOfPostType( 156 | $taxonomy, 157 | static::$post_type, 158 | $params 159 | ); 160 | } 161 | 162 | protected static function hideTitle() 163 | { 164 | Admin::hideTitle(static::$post_type); 165 | } 166 | 167 | protected static function setSideMenuParams(array $params) 168 | { 169 | Admin::setSideMenuParams(static::$post_type, $params); 170 | } 171 | 172 | protected static function setColumns(array $columns) 173 | { 174 | app()->adminColumns()->setPostTypeColumns(static::$post_type, $columns); 175 | } 176 | 177 | protected static function addColumnHandler( 178 | string $name, 179 | callable $handler = null, 180 | string|int $width = 0, 181 | string $css = null 182 | ) { 183 | app()->adminColumns()->addHandler($name, $handler, $width, $css); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Settings/Html.php: -------------------------------------------------------------------------------- 1 | tags from the images and iframes 18 | public static function unwrapParagraphs() 19 | { 20 | \add_filter('the_content', [static::class, '_unwrapParagraphs'], 12); 21 | \add_filter('acf_the_content', [static::class, '_unwrapParagraphs'], 12); 22 | } 23 | public static function _unwrapParagraphs($content) 24 | { 25 | $content = preg_replace('/

\s*()?\s*()\s*(<\/a>)?\s*<\/p>/iU', '\1\2\3', $content); 26 | 27 | return preg_replace('/

\s*(