Repository: octobercms/library
Branch: 4.x
Commit: 21e7461cfd49
Files: 555
Total size: 1.7 MB
Directory structure:
gitextract_egawjsam/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── CREDITS.md
├── LICENSE.md
├── README.md
├── composer.json
├── contracts/
│ ├── Database/
│ │ ├── CurrencyableInterface.php
│ │ ├── MultisiteGroupInterface.php
│ │ ├── MultisiteInterface.php
│ │ ├── NestedSetInterface.php
│ │ ├── SoftDeleteInterface.php
│ │ ├── SortableInterface.php
│ │ ├── SortableRelationInterface.php
│ │ ├── TranslatableInterface.php
│ │ ├── TreeInterface.php
│ │ └── ValidationInterface.php
│ ├── Element/
│ │ ├── FilterElement.php
│ │ ├── FormElement.php
│ │ └── ListElement.php
│ ├── Support/
│ │ └── OctoberPackage.php
│ └── Twig/
│ ├── CallsAnyMethod.php
│ └── CallsMethods.php
├── globals/
│ ├── AjaxException.php
│ ├── App.php
│ ├── ApplicationException.php
│ ├── Arr.php
│ ├── Artisan.php
│ ├── Auth.php
│ ├── Backend.php
│ ├── BackendAuth.php
│ ├── BackendMenu.php
│ ├── BackendUi.php
│ ├── Block.php
│ ├── Broadcast.php
│ ├── Bus.php
│ ├── Cache.php
│ ├── Cms.php
│ ├── Config.php
│ ├── Cookie.php
│ ├── Crypt.php
│ ├── Currency.php
│ ├── Date.php
│ ├── Db.php
│ ├── DbDongle.php
│ ├── Event.php
│ ├── File.php
│ ├── Flash.php
│ ├── ForbiddenException.php
│ ├── Form.php
│ ├── Hash.php
│ ├── Html.php
│ ├── Http.php
│ ├── Ini.php
│ ├── Input.php
│ ├── Lang.php
│ ├── Log.php
│ ├── Mail.php
│ ├── Manifest.php
│ ├── Markdown.php
│ ├── Model.php
│ ├── NotFoundException.php
│ ├── Notification.php
│ ├── Password.php
│ ├── Queue.php
│ ├── Redirect.php
│ ├── Redis.php
│ ├── Request.php
│ ├── Resizer.php
│ ├── Response.php
│ ├── Route.php
│ ├── Schema.php
│ ├── Seeder.php
│ ├── Session.php
│ ├── Site.php
│ ├── Storage.php
│ ├── Str.php
│ ├── System.php
│ ├── SystemException.php
│ ├── Twig.php
│ ├── Ui.php
│ ├── Url.php
│ ├── ValidationException.php
│ ├── Validator.php
│ ├── View.php
│ ├── Vite.php
│ └── Yaml.php
├── init/
│ ├── autoloader.php
│ ├── functions.php
│ ├── init.php
│ └── polyfills.php
├── init.php
├── phpbench.json
├── phpcs.xml
├── phpunit.xml
├── src/
│ ├── Assetic/
│ │ ├── Asset/
│ │ │ ├── AssetCache.php
│ │ │ ├── AssetCollection.php
│ │ │ ├── AssetCollectionInterface.php
│ │ │ ├── AssetInterface.php
│ │ │ ├── BaseAsset.php
│ │ │ ├── FileAsset.php
│ │ │ ├── GlobAsset.php
│ │ │ ├── HttpAsset.php
│ │ │ ├── Iterator/
│ │ │ │ ├── AssetCollectionFilterIterator.php
│ │ │ │ └── AssetCollectionIterator.php
│ │ │ └── StringAsset.php
│ │ ├── AssetManager.php
│ │ ├── AssetWriter.php
│ │ ├── AsseticServiceProvider.php
│ │ ├── Cache/
│ │ │ ├── CacheInterface.php
│ │ │ └── FilesystemCache.php
│ │ ├── Combiner.php
│ │ ├── Factory/
│ │ │ └── AssetFactory.php
│ │ ├── Filter/
│ │ │ ├── BaseCssFilter.php
│ │ │ ├── CssImportFilter.php
│ │ │ ├── CssMinFilter.php
│ │ │ ├── CssRewriteFilter.php
│ │ │ ├── DependencyExtractorInterface.php
│ │ │ ├── FilterCollection.php
│ │ │ ├── FilterInterface.php
│ │ │ ├── HashableInterface.php
│ │ │ ├── JSMinFilter.php
│ │ │ ├── JSqueezeFilter.php
│ │ │ ├── JavascriptImporter.php
│ │ │ ├── LessCompiler.php
│ │ │ ├── LessphpFilter.php
│ │ │ ├── ScssCompiler.php
│ │ │ ├── ScssphpFilter.php
│ │ │ └── StylesheetMinify.php
│ │ ├── FilterManager.php
│ │ ├── README.md
│ │ ├── Traits/
│ │ │ └── HasDeepHasher.php
│ │ └── Util/
│ │ ├── CssUtils.php
│ │ ├── LessUtils.php
│ │ ├── SassUtils.php
│ │ └── VarUtils.php
│ ├── Auth/
│ │ ├── AuthException.php
│ │ ├── Concerns/
│ │ │ ├── HasGuard.php
│ │ │ ├── HasImpersonation.php
│ │ │ ├── HasProviderProxy.php
│ │ │ ├── HasSession.php
│ │ │ ├── HasStatefulGuard.php
│ │ │ ├── HasThrottle.php
│ │ │ └── HasUser.php
│ │ ├── Manager.php
│ │ ├── Migrations/
│ │ │ ├── 2013_10_01_000001_Db_Users.php
│ │ │ ├── 2013_10_01_000002_Db_Groups.php
│ │ │ ├── 2013_10_01_000003_Db_Users_Groups.php
│ │ │ ├── 2013_10_01_000004_Db_Preferences.php
│ │ │ ├── 2013_10_01_000005_Db_Throttle.php
│ │ │ └── 2017_10_01_000006_Db_Roles.php
│ │ └── Models/
│ │ ├── Group.php
│ │ ├── Preferences.php
│ │ ├── Role.php
│ │ ├── Throttle.php
│ │ └── User.php
│ ├── Composer/
│ │ ├── ClassLoader.php
│ │ ├── ComposerManager.php
│ │ ├── Concerns/
│ │ │ ├── HasAssertions.php
│ │ │ ├── HasAutoloader.php
│ │ │ ├── HasOctoberCommands.php
│ │ │ ├── HasOutput.php
│ │ │ └── HasRequirements.php
│ │ └── resources/
│ │ ├── file_get_contents.php
│ │ └── putenv.php
│ ├── Config/
│ │ ├── FileLoader.php
│ │ ├── README.md
│ │ └── Repository.php
│ ├── Database/
│ │ ├── Attach/
│ │ │ ├── File.php
│ │ │ └── FileException.php
│ │ ├── Builder.php
│ │ ├── Collection.php
│ │ ├── Concerns/
│ │ │ ├── HasAttributes.php
│ │ │ ├── HasEagerLoadAttachRelation.php
│ │ │ ├── HasEvents.php
│ │ │ ├── HasJsonable.php
│ │ │ ├── HasNicerPagination.php
│ │ │ ├── HasRelationships.php
│ │ │ └── HasReplication.php
│ │ ├── Connections/
│ │ │ ├── Connection.php
│ │ │ ├── ExtendsConnection.php
│ │ │ ├── MariaDbConnection.php
│ │ │ ├── MySqlConnection.php
│ │ │ ├── PostgresConnection.php
│ │ │ ├── SQLiteConnection.php
│ │ │ └── SqlServerConnection.php
│ │ ├── Connectors/
│ │ │ └── ConnectionFactory.php
│ │ ├── DatabaseServiceProvider.php
│ │ ├── Dongle.php
│ │ ├── ExpandoModel.php
│ │ ├── Factories/
│ │ │ ├── Factory.php
│ │ │ └── HasFactory.php
│ │ ├── Migrations/
│ │ │ ├── 2013_10_01_000001_Db_Deferred_Bindings.php
│ │ │ ├── 2013_10_01_000002_Db_Files.php
│ │ │ ├── 2015_10_01_000003_Db_Revisions.php
│ │ │ └── 2026_10_01_000004_Db_Translate_Attributes.php
│ │ ├── Model.php
│ │ ├── ModelBehavior.php
│ │ ├── ModelException.php
│ │ ├── Models/
│ │ │ ├── DeferredBinding.php
│ │ │ ├── Revision.php
│ │ │ └── TranslateAttribute.php
│ │ ├── MorphPivot.php
│ │ ├── NestedTreeScope.php
│ │ ├── Pivot.php
│ │ ├── QueryBuilder.php
│ │ ├── README.md
│ │ ├── Relations/
│ │ │ ├── AttachMany.php
│ │ │ ├── AttachOne.php
│ │ │ ├── AttachOneOrMany.php
│ │ │ ├── BelongsTo.php
│ │ │ ├── BelongsToMany.php
│ │ │ ├── DeferOneOrMany.php
│ │ │ ├── DefinedConstraints.php
│ │ │ ├── HasMany.php
│ │ │ ├── HasManyThrough.php
│ │ │ ├── HasOne.php
│ │ │ ├── HasOneOrMany.php
│ │ │ ├── HasOneThrough.php
│ │ │ ├── MorphMany.php
│ │ │ ├── MorphOne.php
│ │ │ ├── MorphOneOrMany.php
│ │ │ ├── MorphTo.php
│ │ │ ├── MorphToMany.php
│ │ │ └── Relation.php
│ │ ├── Replicator.php
│ │ ├── Schema/
│ │ │ └── Blueprint.php
│ │ ├── Scopes/
│ │ │ ├── MultisiteGroupScope.php
│ │ │ ├── MultisiteScope.php
│ │ │ ├── NestedTreeScope.php
│ │ │ ├── SoftDeleteScope.php
│ │ │ └── SortableScope.php
│ │ ├── SortableScope.php
│ │ ├── Traits/
│ │ │ ├── BaseIdentifier.php
│ │ │ ├── Defaultable.php
│ │ │ ├── DeferredBinding.php
│ │ │ ├── Encryptable.php
│ │ │ ├── Hashable.php
│ │ │ ├── Multisite.php
│ │ │ ├── MultisiteGroup.php
│ │ │ ├── NestedTree.php
│ │ │ ├── Nullable.php
│ │ │ ├── Purgeable.php
│ │ │ ├── Revisionable.php
│ │ │ ├── SimpleTree.php
│ │ │ ├── Sluggable.php
│ │ │ ├── SluggableTree.php
│ │ │ ├── SoftDelete.php
│ │ │ ├── Sortable.php
│ │ │ ├── SortableRelation.php
│ │ │ ├── Translatable.php
│ │ │ ├── UserFootprints.php
│ │ │ └── Validation.php
│ │ ├── TreeCollection.php
│ │ ├── Updater.php
│ │ └── Updates/
│ │ ├── Migration.php
│ │ └── Seeder.php
│ ├── Element/
│ │ ├── Dash/
│ │ │ └── ReportDefinition.php
│ │ ├── ElementBase.php
│ │ ├── ElementHolder.php
│ │ ├── Filter/
│ │ │ └── ScopeDefinition.php
│ │ ├── Form/
│ │ │ ├── FieldDefinition.php
│ │ │ └── FieldsetDefinition.php
│ │ ├── Lists/
│ │ │ └── ColumnDefinition.php
│ │ ├── Navigation/
│ │ │ └── ItemDefinition.php
│ │ └── OptionDefinition.php
│ ├── Events/
│ │ ├── Dispatcher.php
│ │ ├── EventServiceProvider.php
│ │ ├── FakeDispatcher.php
│ │ └── PriorityDispatcher.php
│ ├── Exception/
│ │ ├── AjaxException.php
│ │ ├── ApplicationException.php
│ │ ├── ErrorHandler.php
│ │ ├── ExceptionBase.php
│ │ ├── ForbiddenException.php
│ │ ├── NotFoundException.php
│ │ ├── SystemException.php
│ │ └── ValidationException.php
│ ├── Extension/
│ │ ├── Container.php
│ │ ├── Extendable.php
│ │ ├── ExtendableTrait.php
│ │ ├── ExtensionBase.php
│ │ ├── ExtensionTrait.php
│ │ └── README.md
│ ├── Filesystem/
│ │ ├── Definitions.php
│ │ ├── Filesystem.php
│ │ ├── FilesystemServiceProvider.php
│ │ ├── README.md
│ │ └── Zip.php
│ ├── Flash/
│ │ ├── FlashBag.php
│ │ └── FlashServiceProvider.php
│ ├── Foundation/
│ │ ├── Application.php
│ │ ├── Bootstrap/
│ │ │ ├── HandleExceptions.php
│ │ │ ├── LoadConfiguration.php
│ │ │ └── RegisterOctober.php
│ │ ├── Configuration/
│ │ │ └── ApplicationBuilder.php
│ │ ├── Console/
│ │ │ ├── ClearCompiledCommand.php
│ │ │ ├── Kernel.php
│ │ │ ├── ProjectSetCommand.php
│ │ │ ├── RouteCacheCommand.php
│ │ │ ├── RouteListCommand.php
│ │ │ └── ServeCommand.php
│ │ ├── Exception/
│ │ │ └── Handler.php
│ │ ├── Http/
│ │ │ ├── Kernel.php
│ │ │ └── Middleware/
│ │ │ ├── CheckForMaintenanceMode.php
│ │ │ └── EncryptCookies.php
│ │ ├── Providers/
│ │ │ ├── AppServiceProvider.php
│ │ │ ├── ArtisanServiceProvider.php
│ │ │ ├── ConsoleSupportServiceProvider.php
│ │ │ ├── CoreServiceProvider.php
│ │ │ ├── DateServiceProvider.php
│ │ │ ├── ExecutionContextProvider.php
│ │ │ └── LogServiceProvider.php
│ │ └── resources/
│ │ └── server.php
│ ├── Halcyon/
│ │ ├── Builder.php
│ │ ├── Collection.php
│ │ ├── Concerns/
│ │ │ └── HasEvents.php
│ │ ├── Datasource/
│ │ │ ├── AutoDatasource.php
│ │ │ ├── Datasource.php
│ │ │ ├── DatasourceInterface.php
│ │ │ ├── DbDatasource.php
│ │ │ ├── FileDatasource.php
│ │ │ ├── Resolver.php
│ │ │ └── ResolverInterface.php
│ │ ├── Exception/
│ │ │ ├── CreateDirectoryException.php
│ │ │ ├── CreateFileException.php
│ │ │ ├── DeleteFileException.php
│ │ │ ├── FileExistsException.php
│ │ │ ├── InvalidDirectoryNameException.php
│ │ │ ├── InvalidExtensionException.php
│ │ │ ├── InvalidFileNameException.php
│ │ │ ├── MissingFileNameException.php
│ │ │ └── ModelException.php
│ │ ├── HalcyonServiceProvider.php
│ │ ├── Migrations/
│ │ │ └── 2021_10_01_000001_Db_Templates.php
│ │ ├── Model.php
│ │ ├── Processors/
│ │ │ ├── Processor.php
│ │ │ └── SectionParser.php
│ │ ├── README.md
│ │ └── Traits/
│ │ └── Validation.php
│ ├── Html/
│ │ ├── BlockBuilder.php
│ │ ├── FormBuilder.php
│ │ ├── Helper.php
│ │ ├── HtmlBuilder.php
│ │ ├── HtmlServiceProvider.php
│ │ ├── README.md
│ │ ├── UrlMixin.php
│ │ └── UrlServiceProvider.php
│ ├── Installer/
│ │ ├── Console/
│ │ │ ├── OctoberBuild.php
│ │ │ └── OctoberInstall.php
│ │ ├── GatewayClient.php
│ │ ├── InstallEventHandler.php
│ │ ├── InstallManager.php
│ │ └── Traits/
│ │ ├── SetupBuilder.php
│ │ └── SetupHelper.php
│ ├── Mail/
│ │ ├── FakeMailer.php
│ │ ├── MailManager.php
│ │ ├── MailParser.php
│ │ ├── MailServiceProvider.php
│ │ ├── Mailable.php
│ │ └── Mailer.php
│ ├── Network/
│ │ └── Http.php
│ ├── Parse/
│ │ ├── Bracket.php
│ │ ├── ComponentParser.php
│ │ ├── Ini.php
│ │ ├── Markdown.php
│ │ ├── MarkdownData.php
│ │ ├── ParseServiceProvider.php
│ │ ├── Parsedown/
│ │ │ ├── Parsedown.php
│ │ │ └── ParsedownExtra.php
│ │ ├── Syntax/
│ │ │ ├── FieldParser.php
│ │ │ ├── Parser.php
│ │ │ ├── README.md
│ │ │ └── SyntaxModelTrait.php
│ │ ├── Twig.php
│ │ └── Yaml.php
│ ├── Resize/
│ │ ├── ResizeBuilder.php
│ │ ├── ResizeServiceProvider.php
│ │ └── Resizer.php
│ ├── Router/
│ │ ├── CoreRedirector.php
│ │ ├── CoreRouter.php
│ │ ├── Helper.php
│ │ ├── README.md
│ │ ├── Router.php
│ │ ├── RoutingServiceProvider.php
│ │ └── Rule.php
│ ├── Scaffold/
│ │ ├── Console/
│ │ │ ├── CreateCommand.php
│ │ │ ├── CreateComponent.php
│ │ │ ├── CreateContentField.php
│ │ │ ├── CreateController.php
│ │ │ ├── CreateFactory.php
│ │ │ ├── CreateFilterWidget.php
│ │ │ ├── CreateFormWidget.php
│ │ │ ├── CreateJob.php
│ │ │ ├── CreateMigration.php
│ │ │ ├── CreateModel.php
│ │ │ ├── CreatePlugin.php
│ │ │ ├── CreateReportWidget.php
│ │ │ ├── CreateSeeder.php
│ │ │ ├── CreateTest.php
│ │ │ ├── command/
│ │ │ │ └── command.stub
│ │ │ ├── component/
│ │ │ │ ├── component.stub
│ │ │ │ └── default.stub
│ │ │ ├── contentfield/
│ │ │ │ └── contentfield.stub
│ │ │ ├── controller/
│ │ │ │ ├── _list_toolbar.stub
│ │ │ │ ├── config_form.stub
│ │ │ │ ├── config_list.stub
│ │ │ │ ├── controller.stub
│ │ │ │ ├── create.stub
│ │ │ │ ├── create_design.stub
│ │ │ │ ├── index.stub
│ │ │ │ ├── preview.stub
│ │ │ │ ├── preview_design.stub
│ │ │ │ ├── update.stub
│ │ │ │ └── update_design.stub
│ │ │ ├── factory/
│ │ │ │ ├── factory.stub
│ │ │ │ └── factory_app.stub
│ │ │ ├── filterwidget/
│ │ │ │ ├── filterwidget.stub
│ │ │ │ ├── javascript.stub
│ │ │ │ ├── partial.stub
│ │ │ │ ├── partial_form.stub
│ │ │ │ └── stylesheet.stub
│ │ │ ├── formwidget/
│ │ │ │ ├── formwidget.stub
│ │ │ │ ├── javascript.stub
│ │ │ │ ├── partial.stub
│ │ │ │ └── stylesheet.stub
│ │ │ ├── job/
│ │ │ │ ├── job.queued.stub
│ │ │ │ └── job.stub
│ │ │ ├── migration/
│ │ │ │ ├── create_app_table.stub
│ │ │ │ ├── create_table.stub
│ │ │ │ ├── update_app_table.stub
│ │ │ │ └── update_table.stub
│ │ │ ├── model/
│ │ │ │ ├── columns.stub
│ │ │ │ ├── fields.stub
│ │ │ │ └── model.stub
│ │ │ ├── plugin/
│ │ │ │ ├── composer.stub
│ │ │ │ ├── plugin.stub
│ │ │ │ └── version.stub
│ │ │ ├── reportwidget/
│ │ │ │ ├── reportwidget.stub
│ │ │ │ └── widget.stub
│ │ │ ├── seeder/
│ │ │ │ ├── create_app_seeder.stub
│ │ │ │ └── create_seeder.stub
│ │ │ └── test/
│ │ │ ├── phpunit.app.stub
│ │ │ ├── phpunit.plugin.stub
│ │ │ └── test.stub
│ │ ├── GeneratorCommand.php
│ │ ├── GeneratorCommandBase.php
│ │ └── ScaffoldServiceProvider.php
│ ├── Support/
│ │ ├── Arr.php
│ │ ├── Collection.php
│ │ ├── Date.php
│ │ ├── Debug/
│ │ │ └── HtmlDumper.php
│ │ ├── DefaultProviders.php
│ │ ├── Facade.php
│ │ ├── Facades/
│ │ │ ├── Auth.php
│ │ │ ├── Block.php
│ │ │ ├── Config.php
│ │ │ ├── Currency.php
│ │ │ ├── DbDongle.php
│ │ │ ├── Event.php
│ │ │ ├── File.php
│ │ │ ├── Flash.php
│ │ │ ├── Form.php
│ │ │ ├── Html.php
│ │ │ ├── Ini.php
│ │ │ ├── Input.php
│ │ │ ├── Mail.php
│ │ │ ├── Markdown.php
│ │ │ ├── Resizer.php
│ │ │ ├── Schema.php
│ │ │ ├── Site.php
│ │ │ ├── Str.php
│ │ │ ├── Twig.php
│ │ │ ├── Url.php
│ │ │ ├── Validator.php
│ │ │ └── Yaml.php
│ │ ├── ModuleServiceProvider.php
│ │ ├── README.md
│ │ ├── SafeCollection.php
│ │ ├── ServiceProvider.php
│ │ ├── Singleton.php
│ │ ├── Str.php
│ │ └── Traits/
│ │ ├── Emitter.php
│ │ ├── KeyParser.php
│ │ └── Singleton.php
│ ├── Translation/
│ │ ├── FileLoader.php
│ │ ├── README.md
│ │ ├── TranslationServiceProvider.php
│ │ └── Translator.php
│ └── Validation/
│ ├── Concerns/
│ │ └── FormatsMessages.php
│ ├── Factory.php
│ ├── ValidationServiceProvider.php
│ └── Validator.php
└── tests/
├── Assetic/
│ ├── MockAsset.php
│ └── StylesheetMinifyTest.php
├── Benchmark/
│ ├── Database/
│ │ └── DatabaseBench.php
│ ├── GeneralBench.php
│ ├── Parse/
│ │ └── ParseBench.php
│ └── Router/
│ └── RouterBench.php
├── Database/
│ ├── DongleTest.php
│ ├── ModelAddersTest.php
│ ├── SortableTest.php
│ ├── Traits/
│ │ ├── EncryptableTest.php
│ │ ├── SluggableTest.php
│ │ └── ValidationTest.php
│ └── UpdaterTest.php
├── Events/
│ └── EventDispatcherTest.php
├── Extension/
│ ├── ExtendableTest.php
│ └── ExtensionTest.php
├── Halcyon/
│ ├── DatasourceResolverTest.php
│ ├── HalcyonModelTest.php
│ ├── SectionParserTest.php
│ └── ValidationTraitTest.php
├── Html/
│ ├── HtmlBuilderTest.php
│ └── HtmlHelperTest.php
├── Mail/
│ └── MailerTest.php
├── Network/
│ └── HttpTest.php
├── Parse/
│ ├── BracketTest.php
│ ├── IniTest.php
│ ├── MarkdownTest.php
│ ├── SyntaxFieldParserTest.php
│ └── SyntaxParserTest.php
├── Router/
│ ├── RouteTest.php
│ └── RouterHelperTest.php
├── Scaffold/
│ └── ScaffoldBaseTest.php
├── Support/
│ ├── CountableTest.php
│ ├── EmitterTest.php
│ └── HttpBuildQueryTest.php
├── TestCase.php
├── Translation/
│ └── TranslatorTest.php
├── fixtures/
│ ├── config/
│ │ └── sample-config.php
│ ├── database/
│ │ └── SampleClass.php
│ ├── halcyon/
│ │ ├── models/
│ │ │ ├── Content.php
│ │ │ ├── Menu.php
│ │ │ └── Page.php
│ │ └── themes/
│ │ ├── theme1/
│ │ │ ├── content/
│ │ │ │ └── welcome.htm
│ │ │ ├── menus/
│ │ │ │ └── mainmenu.htm
│ │ │ └── pages/
│ │ │ ├── about.htm
│ │ │ ├── home.htm
│ │ │ └── level1/
│ │ │ ├── level2/
│ │ │ │ └── level3/
│ │ │ │ └── level4/
│ │ │ │ └── level5/
│ │ │ │ ├── contact.htm
│ │ │ │ └── level6/
│ │ │ │ └── unknown.htm
│ │ │ └── team.htm
│ │ └── theme2/
│ │ └── pages/
│ │ └── home.htm
│ ├── lang/
│ │ └── en/
│ │ └── lang.php
│ └── parse/
│ ├── array.ini
│ ├── basic.ini
│ ├── comments-clean.ini
│ ├── comments.ini
│ ├── complex.ini
│ ├── multilines-value.ini
│ ├── object.ini
│ ├── sections.ini
│ ├── simple.ini
│ └── subsections.ini
└── phpunit.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/tests export-ignore
/.github export-ignore
/phpcs.xml export-ignore
/phpunit.xml export-ignore
/phpbench.json export-ignore
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
push:
branches:
- 2.x
- 3.x
- 4.x
- develop
pull_request:
jobs:
phpUnitTests:
runs-on: ubuntu-latest
strategy:
max-parallel: 6
matrix:
phpVersions: ['8.2', '8.3', '8.4', '8.5']
fail-fast: false
name: PHP ${{ matrix.phpVersions }}
steps:
- name: Checkout changes
uses: actions/checkout@v1
- name: Install PHP
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.phpVersions }}
- name: Install Composer dependencies
run: |
composer config --no-interaction allow-plugins.composer/installers true
composer install --no-interaction --no-progress --no-scripts
- name: Run Tests
run: ./vendor/bin/phpunit ./tests
================================================
FILE: .gitignore
================================================
# Composer files
/vendor
composer.phar
composer.lock
# Editor files
.idea
.vscode
.claude
# Other files
.DS_Store
php_errors.log
.phpunit.result.cache
================================================
FILE: CREDITS.md
================================================
# Credits
This library was created with help from the following packages:
"Laravel", Copyright (c) Taylor Otwell
https://github.com/laravel/framework
"Parsedown", Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
https://github.com/erusev/parsedown
"Assetic", Copyright (c) 2010-2015 OpenSky Project Inc
https://github.com/kriswallsmith/assetic
"http_build_url() for PHP", Copyright (c) 2015 Jake A. Smith
https://github.com/jakeasmith/http_build_url
"Twig extensions", Copyright (c) 2016 Vojta Svoboda
https://github.com/vojtasvoboda/oc-twigextensions-plugin
"October Code", Copyright (c) 2022 Sergey Kasyanov
https://github.com/SergeyKasyanov/vscode-october-extension
================================================
FILE: LICENSE.md
================================================
Copyright (c) 2013-2022 Responsiv Pty Ltd
This End User License Agreement (“EULA”) constitutes a binding agreement between you (the “Licensee”, “you” or “your”) and Responsiv Pty Ltd - ACN 159 492 823 (the “Company”, “we”, “us” or “our”) with respect to your use of the October CMS software (“Licensed Software” or “October CMS Software”). The Company and the Licensee are each individually referred to as “Party” and collectively as “Parties”.
Please carefully read the terms and conditions of this EULA before installing and using the Licensed Software. By using the Licensed Software, you represent that you have read this EULA, and you agree to be bound by all the terms and conditions of this EULA, including any other agreements and policies referenced in this EULA. If you do not agree with any provisions of this EULA, please do not install the October CMS Software.
The Company reserves the right to modify or discontinue the October CMS Software or any portion thereof, temporarily or permanently, with or without notice to you. The Company will not be under any obligation to support or update the Licensed Software, except as described in this EULA.
YOU AGREE THAT THE COMPANY SHALL NOT BE LIABLE TO YOU OR ANY THIRD PARTY IN THE EVENT THAT WE EXERCISE OUR RIGHT TO MODIFY OR DISCONTINUE THE LICENSED SOFTWARE OR ANY PORTION THEREOF.
## Summary
This section outlines some of the key provisions covered in this EULA. Please note that this summary is provided for your convenience only, and it does not relieve you of your obligation to read the full EULA before installing/using the October CMS Software.
By proceeding to use the October CMS Software, you understand and agree that:
- You must be at least 18 years of age to enter into this EULA;
- You will only use the October CMS Software in compliance with applicable laws;
- October CMS Software licenses are only issued for Projects created through the website. To acquire/renew your Project licence, you need to sign in to your October CMS Account, and select/create the Project for which you wish to acquire/renew the licence;
- You will be responsible for paying the full License Fee prior to installing the October CMS Software;
- All License Fee Payments are non-refundable;
- Upon full payment of the License Fee, you will receive a License Key that allows you to install the Licensed Software to create a single production or non-production website and ancillary installations needed to support that single production or non-production website;
- Each new/renewed Project licence comes with one year of Updates. You may continue to use your expired Project license in perpetuity, but if you wish to continue receiving all the Updates, you will be required to keep your Project licence active by renewing it every year;
- Subject to the payment of full License Fee and compliance with this EULA, the Company and its third party licensors grant you a limited, non-exclusive, non-transferable, non-assignable, perpetual and worldwide license to install and/or use the October CMS Software;
- The Company and its licensors retain all rights, title and interest in the October CMS Software, Documentation and other similar proprietary materials made available to you;
- You will not sublicense, resell, distribute, or transfer the Licensed Software to any third party without the Company’s prior written consent;
- You will not remove, obscure or otherwise modify any copyright notices from the October CMS Software files or this EULA;
- You will not modify, disassemble, or reverse engineer any part of the October CMS Software;
- You will take all required steps to prevent unauthorised installation/use of the October CMS Software and prevent any breach of this EULA;
- The Company may terminate this EULA if you are in breach of any provision of this EULA or if we discontinue the October CMS Software;
- SUBJECT TO YOUR STATUTORY RIGHTS, THE COMPANY PROVIDES THE OCTOBER CMS SOFTWARE TO YOU “AS IS” AND “WITH ALL FAULTS”. THE COMPANY DOES NOT OFFER ANY WARRANTIES, WHETHER EXPRESS OR IMPLIED. IN NO EVENT WILL THE COMPANY’S AGGREGATE LIABILITY TO YOU FOR ANY CLAIMS CONNECTED WITH THIS EULA OR THE OCTOBER CMS SOFTWARE EXCEED THE AMOUNT ACTUALLY PAID BY YOU TO THE COMPANY FOR THE OCTOBER CMS SOFTWARE IN THE TWELVE MONTHS PRECEDING THE DATE WHEN THE CLAIM FIRST AROSE;
- This EULA shall be governed by and construed in accordance with the laws of the state of New South Wales, Australia, without giving effect to any principles of conflict of laws.
## Table of Contents
- [1. Definitions](#Definitions)
- [2. Authorisation](#Authorisation)
- [3. License Grant](#License-Grant)
- [4. License Purchase and Renewal](#License-Purchase-and-Renewal)
- [5. License Fees, Payments and Refunds](#License-Fees-Payments-and-Refunds)
- [6. Ownership](#Ownership)
- [7. Use and Restrictions](#Use-and-Restrictions)
- [8. Technical Support](#Technical-Support)
- [9. Termination](#Termination)
- [10. Statutory Consumer Rights](#Statutory-Consumer-Rights)
- [11. Disclaimer of Warranties](#Disclaimer-of-Warranties)
- [12. Limitation of Liability](#Limitation-of-Liability)
- [13. Waiver](#Waiver)
- [14. Indemnification](#Indemnification)
- [15. Compliance with the Laws](#Compliance-with-the-Laws)
- [16. Data Protection](#Data-Protection)
- [17. Delivery](#Delivery)
- [18. General](#General)
## 1. Definitions
The following words shall have the meaning given hereunder whenever they appear in this EULA:
Term | Definition
---- | -----------
Account | refers to a user account registered on the Website by an individual or entity.
Active Term | means the period commencing from the date of License purchase/renewal until the License expiry date as specified on the Project page in your Account.
Documentation | means the current version of the documentation relating to the October CMS Software available at https://docs.octobercms.com or otherwise made available by the Company in electronic format. Documentation includes but is not limited to installation instructions, user guides and help documents regarding the use of the October CMS Software.
License Fee | means the fee payable by the Licensee for the use of the October CMS Software in accordance with the provisions of this EULA and any other applicable agreements referenced in this EULA.
License Key | means a unique string of characters provided by the Company that enables users to install the October CMS Software for a specific Project.
Modifications | means any changes to the original code or files of the October CMS Software by the Licensee or any third party. For the avoidance of any doubt, the term “Modifications” does not include any Updates furnished by the Company.
October CMS Software | refers to the collection of proprietary code and graphics in various file formats provided by the Company to the Licensee.
Project | means a collection of extensions and themes to be used in a single production website.
Updates | means all new releases of the October CMS Software, including but not limited to new features and fixes which are provided by the Company to a Licensee during the Active Term of the License.
The Website | refers to the Company website located at www.octobercms.com.
## 2. Authorisation
You must be at least 18 years of age and have the legal capacity to enter into this EULA. If you are accepting this EULA on behalf of a corporation, organisation, or other legal entity (‘corporate entity’), you warrant that you have the authority to enter into this EULA on behalf of such corporate entity and to bind the former to this EULA.
## 3. License Grant
Subject to your compliance with all the terms and conditions of this EULA and the payment of the full License Fee, the Company and its third party licensors grant you a limited, non-exclusive, non-transferable, non-assignable, perpetual and worldwide license to install and/or use the October CMS Software.
You shall be solely responsible for ensuring that all your authorised employees, contractors or other users of the Licensed Software comply with all the terms and conditions of this EULA, and any failure to comply with this EULA will be deemed as a breach of this EULA by you.
The Company and its third party licensors may make changes to the Licensed Software, which are provided to you through Updates. You will only receive all such Updates during the Active Term of your license. You will not have any right to receive Updates after the expiration of your license. Please note that if you terminate your Account, you will not be able to access your Projects and renew your license/receive any future Updates.
UNLESS EXPRESSLY PROVIDED IN THIS EULA, YOU MAY NOT COPY OR MODIFY THE OCTOBER CMS SOFTWARE.
## 4. License Purchase and Renewal
October CMS Software licenses are only issued for Projects created through the Website. To acquire a new Project licence or to renew an existing Project licence, you must sign in to your October CMS Account and select/create the Project for which you wish to acquire/renew the licence.
Upon full payment of the License Fee, you will receive a License Key that allows you to install the Licensed Software to create a single production or non-production website and ancillary installations needed to support that single production or non-production website.
Each new/renewed Project licence comes with one year of Updates starting from the date on which you pay the License Fee. You are not under any legal obligation to renew your Project licence, and you can continue to use your expired Project license for your website in perpetuity. Please note that expired Project licenses do not receive any Updates. If you wish to continue receiving all the Updates, you will be required to keep your Project licence active by renewing it every year.
By installing and using the October CMS Software, you assume full responsibility for your selection of the Licensed Software, its installation, and the results obtained from the use of the October CMS Software.
## 5. License Fees, Payments and Refunds
The current License Fee for the October CMS Software is published on the Website, and the amount is specified in United States Dollars (USD). The License fee as specified on the Website does not include any taxes which shall be payable by the Licensee in addition to the License Fee.
The License fee becomes due and payable in full at the time the Licensee purchases a new Project license or renews an existing Project license.
All Project licenses are issued/renewed subject to the payment of the License Fee by the Licensee as outlined in Section 7 of our [Website Terms of Use](https://octobercms.com/help/terms/website#fees-payments-refunds-policy) and incorporated into this EULA by reference. The Company reserves the right to refuse issuance of a new Project license or renewal of an existing license until the Company receives the full payment.
To the extent permitted by law, all License Fee payments are non-refundable.
## 6. Ownership
Nothing in this EULA constitutes the sale of October CMS Software to you. The Company and its licensors retain all rights, title and interest in the October CMS Software, Documentation and other similar proprietary materials made available to you (collectively “Proprietary Material”). All Proprietary Material is protected by copyright and other intellectual property laws of Australia and international conventions. You acknowledge that the October CMS Software may contain some open source software that is not owned by the Company and which shall be governed by its own license terms.
Except where authorised by the Company in writing, any use of the October CMS trademark, trade name, or logo is strictly prohibited. The Company reserves all rights that are not expressly granted in and to the Proprietary Material.
## 7. Use and Restrictions
You hereby agree that:
1. A License Key may only be used to create a single production or non-production website as provided in Section 4 (License Purchase and Renewal) of this EULA. If you wish to create another website, you will need to create a new Project and acquire a new License for that Project;
2. Unless expressly provided otherwise in this EULA or other applicable agreements referenced herein, you will not sublicense, resell, distribute, or transfer the Licensed Software to any third party without the Company’s prior written consent;
3. You will not remove, obscure or otherwise modify any copyright notices from the October CMS Software files or this EULA;
4. You will not modify, disassemble, or reverse engineer any part of the October CMS Software;
5. You will take all required steps to prevent unauthorised installation/use of the October CMS Software and prevent any breach of this EULA;
6. You do not receive any rights, interests or titles in the “October CMS” trademark, trade name or service mark (“Marks”), and you will not use any of these Marks without our express consent.
## 8. Technical Support
The Company does not offer any technical support except as described in our Premium Support Policy. The Company reserves the right to deny any and all technical support for any Modifications of the October CMS Software or in the event of any breach of this EULA.
## 9. Termination
This EULA shall remain effective until terminated by either Party as described below.
### 9.1 Termination by Licensee
You may terminate this EULA by uninstalling the October CMS Software and deleting all files and your Account. Please note that once you delete your Account, you will not be able to reactivate it to restore your Projects and access your License Key.
### 9.2 Termination by the Company
The Company may terminate this EULA if you are in breach of any provision of this EULA or if we discontinue the October CMS Software.
### 9.3 Survival
Section 11 (Disclaimer of Warranties), Section 12 (Limitation of Liability), Section 13 (Waiver), Section 14 (Indemnification) and other sections of this EULA that by their nature are intended to survive the termination of this EULA shall survive.
Unless expressly specified otherwise in this EULA, any termination of this EULA either by you or the Company does not create any obligation on the Company to issue a full or partial refund of the License Fee paid by you.
## 10. Statutory Consumer Rights
### 10.1 CONSUMERS IN AUSTRALIA
If you acquire the October CMS Software license as a “consumer”, nothing in this EULA will exclude, limit or modify any rights and guarantees conferred on you by legislation, including the Australian Consumer Law (ACL) in the Competition and Consumer Act 2010 (Cth) (‘Statutory Rights’). If the Company is in breach of any such Statutory Rights, then the Company’s liability shall be limited (at the Company’s option) to:
10.1.1 In case of products supplied to you, to resupplying, replacing or paying the cost of resupplying or replacing the product in respect of which the breach occurred;
10.1.2 In case of services supplied to you, to resupply the service or to pay the cost of resupplying the service in respect of which the breach occurred.
Unless expressly specified otherwise in this EULA, you agree that the Company’s liability for the October CMS Software is governed solely by this EULA and the Australian Consumer Law.
### 10.1 CONSUMERS OUTSIDE OF AUSTRALIA
If you are deemed a “consumer” by statutory law in your country of residence, you may enjoy some legal rights under your local law which prohibit the exclusions, modification or limitations of certain liabilities from applying to you, and where such prohibition exists in your country of residence, any such limitations or exclusions will only apply to you to the extent it is permitted by your local law.
You agree that apart from the application of your statutory consumer rights, the Company’s liability for the October CMS Software is governed solely by this EULA.
## 11. Disclaimer of Warranties
SUBJECT TO YOUR STATUTORY RIGHTS AS PROVIDED IN SECTION 10 ABOVE, THE COMPANY PROVIDES THE OCTOBER CMS SOFTWARE TO YOU “AS IS” AND “WITH ALL FAULTS”.
EXCLUDING ANY EXPRESS WARRANTIES OFFERED IN THIS EULA, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE COMPANY, ITS EMPLOYEES, DIRECTORS, CONTRACTORS, AFFILIATES (“THE COMPANY AND ITS OFFICERS”) DISCLAIM ANY EXPRESS OR IMPLIED WARRANTIES WITH RESPECT TO THE OCTOBER CMS SOFTWARE, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, RELIABILITY, COURSE OF PERFORMANCE OR USAGE IN TRADE. THE COMPANY DOES NOT WARRANT THAT THE OCTOBER CMS SOFTWARE: WILL MEET YOUR REQUIREMENTS; WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE; OR THAT THE COMPANY WILL BE ABLE TO RECTIFY ANY ERRORS, BUGS, SECURITY VULNERABILITIES. NO OBLIGATION, WARRANTIES OR LIABILITY SHALL ARISE OUT OF ANY TECHNICAL SUPPORT SERVICES PROVIDED BY THE COMPANY AND ITS OFFICERS IN CONNECTION WITH THE OCTOBER CMS SOFTWARE. NO VERBAL OR WRITTEN COMMUNICATION RECEIVED FROM THE COMPANY AND ITS OFFICERS, WHETHER MARKETING, PROMOTIONAL OR TECHNICAL SUPPORT, SHALL CREATE ANY WARRANTIES THAT ARE NOT EXPRESSLY PROVIDED IN THIS EULA.
ALTHOUGH THE COMPANY PERIODICALLY RELEASES UPDATES FOR THE OCTOBER CMS SOFTWARE THAT MAY INCLUDE FIXES FOR KNOWN VULNERABILITIES, YOU ACKNOWLEDGE AND AGREE THAT THERE MAY BE VULNERABILITIES THAT THE COMPANY HAS NOT YET IDENTIFIED AND THEREFORE CANNOT ADDRESS. YOU ACKNOWLEDGE AND AGREE THAT YOU ARE SOLELY RESPONSIBLE FOR TAKING ALL THE PRECAUTIONS AND SAFEGUARDS NECESSARY TO PROTECT YOUR WEBSITE AND DATA FROM ANY EXTERNAL ATTACKS, INCLUDING BUT NOT LIMITED TO KEEPING YOUR OCTOBER CMS SOFTWARE INSTALLATION CURRENT AND UP TO DATE.
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO YOU.
## 12. Limitation of Liability
IN NO EVENT SHALL THE COMPANY BE LIABLE TO YOU OR ANY THIRD-PARTY FOR ANY LOSS OF REVENUE, LOSS OF PROFITS (ACTUAL OR ANTICIPATED), LOSS OF SAVINGS (ACTUAL OR ANTICIPATED), LOSS OF OPPORTUNITY, LOSS OF REPUTATION, LOSS OF GOODWILL OR FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES RESULTING FROM THE INSTALLATION, USE OR INABILITY TO USE THE OCTOBER CMS SOFTWARE, WHETHER ARISING FROM ANY BREACH OF CONTRACT, NEGLIGENCE OR ANY OTHER THEORY OF LIABILITY, EVEN IF THE COMPANY WAS PREVIOUSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE LICENSEE SHALL BE SOLELY RESPONSIBLE FOR DETERMINING THE SUITABILITY OF THE LICENSED SOFTWARE AND ALL RISKS ASSOCIATED WITH ITS USE.
IN NO EVENT WILL THE COMPANY’S AGGREGATE LIABILITY TO YOU FOR ANY CLAIMS CONNECTED WITH THIS EULA OR THE OCTOBER CMS SOFTWARE, INCLUDING THOSE ARISING FROM ANY BREACH OF CONTRACT OR NEGLIGENCE, EXCEED THE AMOUNT ACTUALLY PAID BY YOU TO THE COMPANY FOR THE OCTOBER CMS SOFTWARE IN THE TWELVE MONTHS PRECEDING THE DATE WHEN THE CLAIM FIRST AROSE.
NOTWITHSTANDING ANYTHING TO THE FOREGOING, NOTHING IN THIS PROVISION SHALL LIMIT EITHER PARTY’S LIABILITY FOR ANY FRAUD OR LIABILITY THAT CANNOT BE LIMITED BY LAW.
## 13. Waiver
YOU HEREBY RELEASE THE COMPANY AND ITS OFFICERS FROM ALL UNKNOWN RISKS ARISING OUT OF OR ASSOCIATED WITH THE USE OF THE OCTOBER CMS SOFTWARE. IF YOU ARE A RESIDENT IN THE STATE OF CALIFORNIA, U.S.A., YOU EXPRESSLY WAIVE CALIFORNIA CIVIL CODE SECTION 1542, OR OTHER SIMILAR LAW APPLICABLE TO YOU, WHICH STATES: “A GENERAL RELEASE DOES NOT EXTEND TO CLAIMS WHICH THE CREDITOR DOES NOT KNOW OR SUSPECT TO EXIST IN HIS OR HER FAVOR AT THE TIME OF EXECUTING THE RELEASE, WHICH IF KNOWN BY HIM OR HER MUST HAVE MATERIALLY AFFECTED HIS OR HER SETTLEMENT WITH THE DEBTOR OR RELEASED PARTY. ”
**YOU ACKNOWLEDGE AND AGREE THAT THE LIMITATION OF LIABILITY AND THE WAIVER SET FORTH ABOVE REFLECT A REASONABLE AND FAIR ALLOCATION OF RISK BETWEEN YOU AND THE COMPANY AND THAT THESE PROVISIONS FORM AN ESSENTIAL BASIS OF THE BARGAIN BETWEEN YOU AND THE COMPANY. THE COMPANY WOULD NOT BE ABLE TO PROVIDE THE OCTOBER CMS SOFTWARE TO YOU ON AN ECONOMICALLY REASONABLE BASIS WITHOUT THESE LIMITATIONS.**
## 14. Indemnification
14.1 The Company will defend or settle any claims against you that allege that the October CMS Software as supplied to you for installation and use in accordance with this EULA and Documentation infringes the intellectual property rights of a third party provided that:
14.1.1 You immediately notify the Company of any such claim that relates to this indemnity;
14.1.2 The Company has the sole right to control the defence or settlement of such claim; and
14.1.3 You provide the Company with all reasonable assistance in relation to the defence.
14.2 For any claims of infringement of intellectual property mentioned in Section 14.1, the Company reserves the right to:
14.2.1 Modify or replace the Licensed Software to make it non-infringing provided such modification or replacement does not substantively change the functionality of the Licensed Software; or
14.2.2 Acquire at its own expense the right for you to continue the use of the Licensed Software; or
14.2.3 Terminate the Project license and direct you to cease the use of the Licensed Software. In such cases, the Company will provide you with a full refund of the License Fees paid by you for the Licensed Software in the preceding 12 months. Please note that where the Company directs you to cease the use of the October CMS Software due to a third party claim of infringement, you are under a legal obligation to immediately cease such use.
Unless otherwise provided by law, this Section 14 sets out your exclusive remedies for any infringement of intellectual property claims made by a third party against you and nothing in this EULA will create any obligations on the Company to offer greater indemnity. The Company does not have any obligation to defend or indemnify any other third party.
14.3 The Company shall not have any obligation to indemnify you if:
14.3.1 The infringement arises out of any Modification of the October CMS Software;
14.3.2 The infringement arises from any combination, operation, or use of the Licensed Software with any other third party software;
14.3.3 The infringement arises from the use of the Licensed Software in breach of this EULA;
14.3.4 The infringement arises from the use of the Licensed Software after the Company has informed you to cease the use of the Licensed Software due to possible claims.
14.4 You will indemnify the Company against any third party claims, damages, losses and costs, including reasonable attorney fees arising from:
14.4.1 Your actions or omissions (actual or alleged), including without limitation, claims that your actions or omission infringe any third party’s intellectual property rights; or
14.4.2 Your violation of any applicable laws; or
14.4.3 Your breach of this EULA.
## 15. Compliance with the Laws
You will only install/use the October CMS Software and fulfil all your obligations under this EULA in compliance with all applicable laws. You hereby confirm that neither you nor the corporate entity that you represent is subject or target of any government Sanctions, and your use of the October CMS Software would not result in violation of any Sanctions by the Company.
You further confirm that the Licensed Software will not be used by any individual or entity engaged in any of the following activities: (i) Terrorist activities; (ii) design, development or production of any weapons of mass destruction; or (iii) any other illegal activity.
## 16. Data Protection
The Company only collects minimal personal data from the Licensee, as described in our Privacy Policy. The Company does not process any data on behalf of the Licensee, and the Licensee shall be solely responsible for compliance with applicable data protection laws for any data it controls or processes.
## 17. Delivery
The Company will deliver the October CMS Software and this EULA to you by electronic download.
## 18. General
### 18.1 Amendments
The Company reserves the right to amend the terms and conditions of this EULA at any time and without giving any prior notice to you. You acknowledge that you are responsible for periodically reviewing this EULA to familiarise yourself with any changes. Your continued use of the October CMS Software after any changes to the EULA shall constitute your consent to such change. You can access the latest version of the EULA by visiting https://octobercms.com/eula
### 18.2 Assignment
You may not assign any rights and obligations under this EULA, in whole or in part, without an authorised Company representative's written consent. Any attempt to assign any rights and obligations without the Company's consent shall be void. The Company reserves the right to assign any of its rights and obligations under this EULA to a third party without requiring your consent.
### 18.3 Notices
You hereby consent to receive all notices and communication from the Company electronically.
All notices to the Company under this EULA shall be sent to:
PO Box 47
Queanbeyan NSW 2620
Australia
For any other questions relating to this EULA, please contact us at https://octobercms.com/contact
### 18.4 Governing Law and Jurisdiction
This EULA shall be governed by and construed in accordance with the laws of the state of New South Wales, Australia, without giving effect to any principles of conflict of laws. The parties hereby agree to submit to the non-exclusive jurisdiction of the courts of New South Wales to decide any matter arising out of these Terms. Both Parties hereby agree that the United Nations Convention on Contracts for the International Sale of Goods will not apply to this EULA.
### 18.5 Force Majeure
Neither Party will be liable to the other for any failure or delay in the performance of its obligations to the extent that such failure or delay is caused by any unforeseen events which are beyond the reasonable control of the obligated Party such as an act of God, strike, war, terrorism, epidemic, internet or telecommunication outage or other similar events, and the obligated Party is not able to avoid or remove the force measure by taking reasonable measures.
================================================
FILE: README.md
================================================
October Rain
=======
This repository contains the core library of October CMS. If you want to build a website using October, visit the main [October repository](http://github.com/octobercms/october).
## License
The October CMS platform is licensed software, see [End User License Agreement](./LICENSE.md) (EULA) for more details.
================================================
FILE: composer.json
================================================
{
"name": "october/rain",
"description": "October Rain Library",
"homepage": "http://octobercms.com",
"keywords": ["october", "cms", "rain"],
"authors": [
{
"name": "Alexey Bobkov",
"email": "aleksey.bobkov@gmail.com"
},
{
"name": "Samuel Georges",
"email": "daftspunky@gmail.com"
}
],
"require": {
"php": "^8.1",
"composer/composer": "^2.0.0",
"composer/installers": "^1 || ^2",
"larajax/larajax": "^2.0",
"doctrine/dbal": "^2.13.3|^3.1.4",
"intervention/image": "^3.10",
"jaybizzle/crawler-detect": "^1.3",
"linkorb/jsmin-php": "~1.0",
"wikimedia/less.php": "~5.2",
"scssphp/scssphp": "~1.0",
"symfony/yaml": "^6.4|^7.0",
"twig/twig": "^3.21",
"league/csv": "~9.1",
"laravel/tinker": "~2.0|~3.0",
"symfony/html-sanitizer": "^6.1|^7.0",
"enshrined/svg-sanitize": "^0.22"
},
"require-dev": {
"laravel/framework": "^12.0",
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0|^12.5.22",
"meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0",
"phpbench/phpbench": "^1.4"
},
"autoload": {
"files": [
"init/init.php"
],
"classmap": [
"globals/"
],
"psr-4": {
"October\\Rain\\": "src/",
"October\\Contracts\\": "contracts/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},
"scripts": {
"test": [
"phpunit --stop-on-failure"
],
"bench": [
"phpbench run tests\\Benchmark\\ --report=default"
]
},
"extra": {
"laravel": {
"providers": [
"October\\Rain\\Foundation\\Providers\\CoreServiceProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
================================================
FILE: contracts/Database/CurrencyableInterface.php
================================================
[],
* 'functions' => []
* ];
*
* @return array
*/
public function registerMarkupTags();
/**
* registerComponents registers any CMS components implemented in this package.
*
* return [
* \Acme\Demo\Components\LocalePicker::class => 'localePicker',
* ];
*
* @return array
*/
public function registerComponents();
/**
* registerPageSnippets registers any CMS snippets implemented in this package.
*
* return [
* \Acme\Demo\Components\YouTubeVideo::class => 'youtubeVideo',
* ];
*
* @return array
*/
public function registerPageSnippets();
/**
* registerContentFields registers content fields used by tailor implemented in this package.
*
* return [
* \Tailor\ContentFields\TextareaField::class => 'textarea',
* ];
*
* @return array
*/
public function registerContentFields();
/**
* registerNavigation registers backend navigation items for this package.
*
* return [
* 'blog' => []
* ];
*
* @return array
*/
public function registerNavigation();
/**
* registerPermissions registers any permissions used by this package.
*
* return [
* 'general.backend' => [
* 'label' => 'Access the Backend Panel',
* 'tab' => 'General'
* ],
* ];
*
* @return array
*/
public function registerPermissions();
/**
* registerSettings registers any backend configuration links used by this package.
*
* return [
* 'updates' => []
* ];
*
* @return array
*/
public function registerSettings();
/**
* registerReportWidgets registers any report widgets provided by this package.
* The widgets must be returned in the following format:
*
* return [
* 'className1' => [
* 'label' => 'My widget 1',
* 'context' => ['context-1', 'context-2'],
* ],
* 'className2' => [
* 'label' => 'My widget 2',
* 'context' => 'context-1'
* ]
* ];
*
* @return array
*/
public function registerReportWidgets();
/**
* registerFormWidgets registers any form widgets implemented in this package.
* The widgets must be returned in the following format:
*
* return [
* ['className1' => 'alias'],
* ['className2' => 'anotherAlias']
* ];
*
* @return array
*/
public function registerFormWidgets();
/**
* registerFilterWidgets registers any filter widgets implemented in this package.
* The widgets must be returned in the following format:
*
* return [
* ['className1' => 'alias'],
* ['className2' => 'anotherAlias']
* ];
*
* @return array
*/
public function registerFilterWidgets();
/**
* registerListColumnTypes registers custom backend list column types introduced
* by this package.
*
* @return array
*/
public function registerListColumnTypes();
/**
* registerMailLayouts registers any mail layouts implemented by this package.
* The layouts must be returned in the following format:
*
* return [
* 'marketing' => 'acme.blog::layouts.marketing',
* 'notification' => 'acme.blog::layouts.notification',
* ];
*
* @return array
*/
public function registerMailLayouts();
/**
* registerMailTemplates registers any mail templates implemented by this package.
* The templates must be returned in the following format:
*
* return [
* 'acme.blog::mail.welcome',
* 'acme.blog::mail.forgot_password',
* ];
*
* @return array
*/
public function registerMailTemplates();
/**
* registerMailPartials registers any mail partials implemented by this package.
* The partials must be returned in the following format:
*
* return [
* 'tracking' => 'acme.blog::partials.tracking',
* 'promotion' => 'acme.blog::partials.promotion',
* ];
*
* @return array
*/
public function registerMailPartials();
/**
* registerSchedule registers scheduled tasks that are executed on a regular basis.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function registerSchedule($schedule);
}
================================================
FILE: contracts/Twig/CallsAnyMethod.php
================================================
withNamespace('App\\', 'app')
->withDirectories([
'modules',
'plugins'
])
->register();
================================================
FILE: init/functions.php
================================================
$binding) {
if ($binding instanceof \DateTime) {
$bindings[$i] = $binding->format('\'Y-m-d H:i:s\'');
}
elseif (is_string($binding)) {
$bindings[$i] = "'$binding'";
}
}
$query = str_replace(['%', '?'], ['%%', '%s'], $query);
$query = vsprintf($query, $bindings);
Log::info($query);
});
}
}
if (!function_exists('traceSql')) {
/**
* traceSql is an alias for trace_sql()
* @return void
*/
function traceSql()
{
trace_sql();
}
}
if (!function_exists('traceBack')) {
/**
* traceBack is an alias for trace_back()
*/
function traceBack(int $distance = 25)
{
trace_back($distance);
}
}
if (!function_exists('trace_back')) {
/**
* trace_back logs a simple backtrace from the call point
* @return void
*/
function trace_back(int $distance = 25)
{
trace_log(debug_backtrace(2, $distance));
}
}
if (!function_exists('plugins_path')) {
/**
* plugins_path gets the path to the plugins folder
* @param string $path
* @return string
*/
function plugins_path($path = '')
{
return app('path.plugins').($path ? '/'.$path : $path);
}
}
if (!function_exists('cache_path')) {
/**
* cache_path gets the path to the cache folder
* @param string $path
* @return string
*/
function cache_path($path = '')
{
return app('path.cache').($path ? '/'.$path : $path);
}
}
if (!function_exists('themes_path')) {
/**
* themes_path gets the path to the themes folder
* @param string $path
* @return string
*/
function themes_path($path = '')
{
return app('path.themes').($path ? '/'.$path : $path);
}
}
if (!function_exists('temp_path')) {
/**
* temp_path gets the path to the temporary storage folder
* @param string $path
* @return string
*/
function temp_path($path = '')
{
return app('path.temp').($path ? '/'.$path : $path);
}
}
if (!function_exists('e')) {
/**
* e will encode HTML special characters in a string
* @param \Illuminate\Contracts\Support\Htmlable|string $value
* @param bool $doubleEncode
* @return string
*/
function e($value, $doubleEncode = false)
{
if ($value instanceof \Illuminate\Contracts\Support\Htmlable) {
return $value->toHtml();
}
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', $doubleEncode);
}
}
if (!function_exists('trans')) {
/**
* trans translates the given message
* @param string $id
* @param array $parameters
* @param string $locale
* @return string
*/
function trans($id = null, $parameters = [], $locale = null)
{
return app('translator')->trans($id, $parameters, $locale);
}
}
if (!function_exists('array_build')) {
/**
* array_build builds a new array using a callback
* @param array $array
* @param callable $callback
* @return array
*/
function array_build($array, callable $callback)
{
return Arr::build($array, $callback);
}
}
if (!function_exists('collect')) {
/**
* collect creates a collection from the given value
* @param mixed $value
* @return \October\Rain\Support\Collection
*/
function collect($value = null)
{
return new Collection($value);
}
}
if (!function_exists('array_add')) {
/**
* array_add adds an element to an array using "dot" notation if it doesn't exist
* @param array $array
* @param string $key
* @param mixed $value
* @return array
*/
function array_add($array, $key, $value)
{
return Arr::add($array, $key, $value);
}
}
if (!function_exists('array_collapse')) {
/**
* array_collapse collapses an array of arrays into a single array
* @param array $array
* @return array
*/
function array_collapse($array)
{
return Arr::collapse($array);
}
}
if (!function_exists('array_divide')) {
/**
* array_divide divides an array into two arrays. One with keys and the other with values
* @param array $array
* @return array
*/
function array_divide($array)
{
return Arr::divide($array);
}
}
if (!function_exists('array_dot')) {
/**
* array_dot flattens a multi-dimensional associative array with dots
* @param array $array
* @param string $prepend
* @return array
*/
function array_dot($array, $prepend = '')
{
return Arr::dot($array, $prepend);
}
}
if (!function_exists('array_except')) {
/**
* array_except gets all of the given array except for a specified array of keys
* @param array $array
* @param array|string $keys
* @return array
*/
function array_except($array, $keys)
{
return Arr::except($array, $keys);
}
}
if (!function_exists('array_flatten')) {
/**
* array_flatten flattens a multi-dimensional array into a single level
* @param array $array
* @param int $depth
* @return array
*/
function array_flatten($array, $depth = PHP_INT_MAX)
{
return Arr::flatten($array, $depth);
}
}
if (!function_exists('array_forget')) {
/**
* array_forget removes one or many array items from a given array using "dot" notation
* @param array $array
* @param array|string $keys
* @return void
*/
function array_forget(&$array, $keys)
{
Arr::forget($array, $keys);
}
}
if (!function_exists('array_get')) {
/**
* array_get gets an item from an array using "dot" notation
* @param \ArrayAccess|array $array
* @param string|int $key
* @param mixed $default
* @return mixed
*/
function array_get($array, $key, $default = null)
{
return Arr::get($array, $key, $default);
}
}
if (!function_exists('array_has')) {
/**
* array_has checks if an item or items exist in an array using "dot" notation
* @param \ArrayAccess|array $array
* @param string|array $keys
* @return bool
*/
function array_has($array, $keys)
{
return Arr::has($array, $keys);
}
}
if (!function_exists('array_only')) {
/**
* array_only gets a subset of the items from the given array
* @param array $array
* @param array|string $keys
* @return array
*/
function array_only($array, $keys)
{
return Arr::only($array, $keys);
}
}
if (!function_exists('array_pluck')) {
/**
* array_pluck plucks an array of values from an array
* @param array $array
* @param string|array $value
* @param string|array|null $key
* @return array
*/
function array_pluck($array, $value, $key = null)
{
return Arr::pluck($array, $value, $key);
}
}
if (!function_exists('array_prepend')) {
/**
* array_prepend pushes an item onto the beginning of an array
* @param array $array
* @param mixed $value
* @param mixed $key
* @return array
*/
function array_prepend($array, $value, $key = null)
{
return Arr::prepend($array, $value, $key);
}
}
if (!function_exists('array_pull')) {
/**
* array_pull gets a value from the array, and remove it
* @param array $array
* @param string $key
* @param mixed $default
* @return mixed
*/
function array_pull(&$array, $key, $default = null)
{
return Arr::pull($array, $key, $default);
}
}
if (!function_exists('array_random')) {
/**
* array_random gets a random value from an array
* @param array $array
* @param int|null $num
* @return mixed
*/
function array_random($array, $num = null)
{
return Arr::random($array, $num);
}
}
if (!function_exists('array_set')) {
/**
* array_set sets an array item to a given value using "dot" notation
* If no key is given to the method, the entire array will be replaced.
* @param array $array
* @param string $key
* @param mixed $value
* @return array
*/
function array_set(&$array, $key, $value)
{
return Arr::set($array, $key, $value);
}
}
if (!function_exists('array_sort')) {
/**
* array_sort sorts the array by the given callback or attribute name
* @param array $array
* @param callable|string|null $callback
* @return array
*/
function array_sort($array, $callback = null)
{
return Arr::sort($array, $callback);
}
}
if (!function_exists('array_sort_recursive')) {
/**
* array_sort_recursive recursively sort an array by keys and values
* @param array $array
* @return array
*/
function array_sort_recursive($array)
{
return Arr::sortRecursive($array);
}
}
if (!function_exists('array_where')) {
/**
* array_where filters the array using the given callback
* @param array $array
* @param callable $callback
* @return array
*/
function array_where($array, callable $callback)
{
return Arr::where($array, $callback);
}
}
if (!function_exists('array_wrap')) {
/**
* array_wrap if the given value is not an array, wrap it in one
* @param mixed $value
* @return array
*/
function array_wrap($value)
{
return Arr::wrap($value);
}
}
if (!function_exists('camel_case')) {
/**
* camel_case converts a value to camel case
* @param string $value
* @return string
*/
function camel_case($value)
{
return Str::camel($value);
}
}
if (!function_exists('kebab_case')) {
/**
* kebab_case converts a string to kebab case
* @param string $value
* @return string
*/
function kebab_case($value)
{
return Str::kebab($value);
}
}
if (!function_exists('snake_case')) {
/**
* snake_case converts a string to snake case
* @param string $value
* @param string $delimiter
* @return string
*/
function snake_case($value, $delimiter = '_')
{
return Str::snake($value, $delimiter);
}
}
if (!function_exists('str_after')) {
/**
* str_after returns the remainder of a string after a given value
* @param string $subject
* @param string $search
* @return string
*/
function str_after($subject, $search)
{
return Str::after($subject, $search);
}
}
if (!function_exists('str_before')) {
/**
* str_before get the portion of a string before a given value
* @param string $subject
* @param string $search
* @return string
*/
function str_before($subject, $search)
{
return Str::before($subject, $search);
}
}
if (!function_exists('str_finish')) {
/**
* str_finish caps a string with a single instance of a given value
* @param string $value
* @param string $cap
* @return string
*/
function str_finish($value, $cap)
{
return Str::finish($value, $cap);
}
}
if (!function_exists('str_is')) {
/**
* str_is determines if a given string matches a given pattern
* @param string|array $pattern
* @param string $value
* @return bool
*/
function str_is($pattern, $value)
{
return Str::is($pattern, $value);
}
}
if (!function_exists('str_limit')) {
/**
* str_limit limits the number of characters in a string
* @param string $value
* @param int $limit
* @param string $end
* @return string
*/
function str_limit($value, $limit = 100, $end = '...')
{
return Str::limit($value, $limit, $end);
}
}
if (!function_exists('str_plural')) {
/**
* str_plural get the plural form of an English word
* @param string $value
* @param int $count
* @return string
*/
function str_plural($value, $count = 2)
{
return Str::plural($value, $count);
}
}
if (!function_exists('str_random')) {
/**
* str_random generates a more truly "random" alpha-numeric string
* @param int $length
* @return string
*
* @throws \RuntimeException
*/
function str_random($length = 16)
{
return Str::random($length);
}
}
if (!function_exists('str_replace_array')) {
/**
* str_replace_array replaces a given value in the string sequentially with an array
* @param string $search
* @param array $replace
* @param string $subject
* @return string
*/
function str_replace_array($search, array $replace, $subject)
{
return Str::replaceArray($search, $replace, $subject);
}
}
if (!function_exists('str_replace_first')) {
/**
* str_replace_first replaces the first occurrence of a given value in the string
* @param string $search
* @param string $replace
* @param string $subject
* @return string
*/
function str_replace_first($search, $replace, $subject)
{
return Str::replaceFirst($search, $replace, $subject);
}
}
if (!function_exists('str_replace_last')) {
/**
* str_replace_last replaces the last occurrence of a given value in the string
* @param string $search
* @param string $replace
* @param string $subject
* @return string
*/
function str_replace_last($search, $replace, $subject)
{
return Str::replaceLast($search, $replace, $subject);
}
}
if (!function_exists('str_singular')) {
/**
* str_singular get the singular form of an English word
* @param string $value
* @return string
*/
function str_singular($value)
{
return Str::singular($value);
}
}
if (!function_exists('str_slug')) {
/**
* str_slug generates a URL friendly "slug" from a given string
* @param string $title
* @param string $separator
* @param string $language
* @return string
*/
function str_slug($title, $separator = '-', $language = 'en')
{
return Str::slug($title, $separator, $language);
}
}
if (!function_exists('str_start')) {
/**
* str_start begins a string with a single instance of a given value
* @param string $value
* @param string $prefix
* @return string
*/
function str_start($value, $prefix)
{
return Str::start($value, $prefix);
}
}
if (!function_exists('studly_case')) {
/**
* studly_case converts a value to studly caps case
* @param string $value
* @return string
*/
function studly_case($value)
{
return Str::studly($value);
}
}
if (!function_exists('title_case')) {
/**
* title_case converts a value to title case
* @param string $value
* @return string
*/
function title_case($value)
{
return Str::title($value);
}
}
if (!function_exists('link_to')) {
/**
* Generate a HTML link.
*
* @param string $url
* @param string $title
* @param array $attributes
* @param bool $secure
* @return string
*/
function link_to($url, $title = null, $attributes = [], $secure = null)
{
return app('html')->link($url, $title, $attributes, $secure);
}
}
if (!function_exists('link_to_asset')) {
/**
* Generate a HTML link to an asset.
*
* @param string $url
* @param string $title
* @param array $attributes
* @param bool $secure
* @return string
*/
function link_to_asset($url, $title = null, $attributes = [], $secure = null)
{
return app('html')->linkAsset($url, $title, $attributes, $secure);
}
}
if (!function_exists('link_to_route')) {
/**
* Generate a HTML link to a named route.
*
* @param string $name
* @param string $title
* @param array $parameters
* @param array $attributes
* @return string
*/
function link_to_route($name, $title = null, $parameters = [], $attributes = [])
{
return app('html')->linkRoute($name, $title, $parameters, $attributes);
}
}
if (!function_exists('link_to_action')) {
/**
* Generate a HTML link to a controller action.
*
* @param string $action
* @param string $title
* @param array $parameters
* @param array $attributes
* @return string
*/
function link_to_action($action, $title = null, $parameters = [], $attributes = [])
{
return app('html')->linkAction($action, $title, $parameters, $attributes);
}
}
if (!function_exists('starts_with')) {
/**
* @deprecated use str_starts_with
*/
function starts_with($haystack, $needles)
{
return Str::startsWith($haystack, $needles);
}
}
if (!function_exists('ends_with')) {
/**
* @deprecated use str_ends_with
*/
function ends_with($haystack, $needles)
{
return Str::endsWith($haystack, $needles);
}
}
if (!function_exists('asset_version')) {
/**
* asset_version takes a disk path, resolves it to a public URL, and appends
* a cache-busting version query string based on the file's modification time.
*
* Supports path symbols: ~ (base), $ (plugins), # (themes)
*
* asset_version('~/themes/demo/assets/js/app.js')
* // → http://localhost/themes/demo/assets/js/app.js?v1a2b3c4d
*
* @param string $path
* @return string
*/
function asset_version(string $path): string
{
return Url::assetVersion($path);
}
}
================================================
FILE: init/init.php
================================================
The coding standard for October CMS.*/src/Auth/Migrations/*\.php*/src/Database/Migrations/*\.php*/tests/**/tests/*src/tests/*/vendor/**/tests/fixtures/config/sample-config.php
================================================
FILE: phpunit.xml
================================================
./tests/**
================================================
FILE: src/Assetic/Asset/AssetCache.php
================================================
*/
class AssetCache implements AssetInterface
{
/**
* @var AssetInterface asset
*/
protected $asset;
/**
* @var CacheInterface cache
*/
protected $cache;
/**
* __construct
*/
public function __construct(AssetInterface $asset, CacheInterface $cache)
{
$this->asset = $asset;
$this->cache = $cache;
}
/**
* ensureFilter
*/
public function ensureFilter(FilterInterface $filter): void
{
$this->asset->ensureFilter($filter);
}
/**
* getFilters
*/
public function getFilters(): array
{
return $this->asset->getFilters();
}
/**
* clearFilters
*/
public function clearFilters(): void
{
$this->asset->clearFilters();
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
$cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'load');
if ($this->cache->has($cacheKey)) {
$this->asset->setContent($this->cache->get($cacheKey));
return;
}
$this->asset->load($additionalFilter);
$this->cache->set($cacheKey, $this->asset->getContent());
}
/**
* dump
*/
public function dump(?FilterInterface $additionalFilter = null): string
{
$cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'dump');
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$content = $this->asset->dump($additionalFilter);
$this->cache->set($cacheKey, $content);
return $content;
}
/**
* getContent
*/
public function getContent(): ?string
{
return $this->asset->getContent();
}
/**
* setContent
*/
public function setContent(?string $content): void
{
$this->asset->setContent($content);
}
/**
* getSourceRoot
*/
public function getSourceRoot(): ?string
{
return $this->asset->getSourceRoot();
}
/**
* getSourcePath
*/
public function getSourcePath(): ?string
{
return $this->asset->getSourcePath();
}
/**
* getSourceDirectory
*/
public function getSourceDirectory(): ?string
{
return $this->asset->getSourceDirectory();
}
/**
* getTargetPath
*/
public function getTargetPath(): ?string
{
return $this->asset->getTargetPath();
}
/**
* setTargetPath
*/
public function setTargetPath(?string $targetPath): void
{
$this->asset->setTargetPath($targetPath);
}
/**
* getLastModified
*/
public function getLastModified(): ?int
{
return $this->asset->getLastModified();
}
/**
* getVars
*/
public function getVars(): array
{
return $this->asset->getVars();
}
/**
* setValues
*/
public function setValues(array $values): void
{
$this->asset->setValues($values);
}
/**
* getValues
*/
public function getValues(): array
{
return $this->asset->getValues();
}
/**
* getCacheKey returns a cache key for the current asset.
* The key is composed of everything but an asset's content:
*
* * source root
* * source path
* * target url
* * last modified
* * filters
*
* @param AssetInterface $asset The asset
* @param FilterInterface $additionalFilter Any additional filter being applied
* @param string $salt Salt for the key
*
* @return string A key for identifying the current asset
*/
protected static function getCacheKey(AssetInterface $asset, ?FilterInterface $additionalFilter = null, string $salt = ''): string
{
if ($additionalFilter) {
$asset = clone $asset;
$asset->ensureFilter($additionalFilter);
}
$cacheKey = $asset->getSourceRoot();
$cacheKey .= $asset->getSourcePath();
$cacheKey .= $asset->getTargetPath();
$cacheKey .= $asset->getLastModified();
foreach ($asset->getFilters() as $filter) {
if ($filter instanceof HashableInterface) {
$cacheKey .= $filter->hash();
}
else {
$cacheKey .= serialize($filter);
}
}
if ($values = $asset->getValues()) {
asort($values);
$cacheKey .= serialize($values);
}
return md5($cacheKey.$salt);
}
}
================================================
FILE: src/Assetic/Asset/AssetCollection.php
================================================
*/
class AssetCollection implements IteratorAggregate, AssetCollectionInterface
{
/**
* @var array assets
*/
protected $assets;
/**
* @var FilterCollection filters
*/
protected $filters;
/**
* @var string|null sourceRoot
*/
protected $sourceRoot;
/**
* @var string|null targetPath
*/
protected $targetPath;
/**
* @var string|null content
*/
protected $content;
/**
* @var SplObjectStorage clones
*/
protected $clones;
/**
* @var array vars
*/
protected $vars;
/**
* @var array values
*/
protected $values;
/**
* __construct
*
* @param array $assets Assets for the current collection
* @param array $filters Filters for the current collection
* @param string $sourceRoot The root directory
* @param array $vars
*/
public function __construct(array $assets = [], array $filters = [], ?string $sourceRoot = null, array $vars = [])
{
$this->assets = [];
foreach ($assets as $asset) {
$this->add($asset);
}
$this->filters = new FilterCollection($filters);
$this->sourceRoot = $sourceRoot;
$this->clones = new SplObjectStorage();
$this->vars = $vars;
$this->values = [];
}
/**
* __clone
*/
public function __clone()
{
$this->filters = clone $this->filters;
$this->clones = new SplObjectStorage();
}
/**
* all
*/
public function all(): array
{
return $this->assets;
}
/**
* add
*/
public function add(AssetInterface $asset): void
{
$this->assets[] = $asset;
}
/**
* removeLeaf
*/
public function removeLeaf(AssetInterface $needle, bool $graceful = false): bool
{
foreach ($this->assets as $i => $asset) {
$clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null;
if (in_array($needle, [$asset, $clone], true)) {
unset($this->clones[$asset], $this->assets[$i]);
return true;
}
if ($asset instanceof AssetCollectionInterface && $asset->removeLeaf($needle, true)) {
return true;
}
}
if ($graceful) {
return false;
}
throw new InvalidArgumentException('Leaf not found.');
}
/**
* replaceLeaf
*/
public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool
{
foreach ($this->assets as $i => $asset) {
$clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null;
if (in_array($needle, [$asset, $clone], true)) {
unset($this->clones[$asset]);
$this->assets[$i] = $replacement;
return true;
}
if ($asset instanceof AssetCollectionInterface && $asset->replaceLeaf($needle, $replacement, true)) {
return true;
}
}
if ($graceful) {
return false;
}
throw new InvalidArgumentException('Leaf not found.');
}
/**
* ensureFilter
*/
public function ensureFilter(FilterInterface $filter): void
{
$this->filters->ensure($filter);
}
/**
* getFilters
*/
public function getFilters(): array
{
return $this->filters->all();
}
/**
* clearFilters
*/
public function clearFilters(): void
{
$this->filters->clear();
$this->clones = new SplObjectStorage();
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
// loop through leaves and load each asset
$parts = [];
foreach ($this as $asset) {
$asset->load($additionalFilter);
$parts[] = $asset->getContent();
}
$this->content = implode("\n", $parts);
}
/**
* dump
*/
public function dump(?FilterInterface $additionalFilter = null): string
{
// loop through leaves and dump each asset
$parts = [];
foreach ($this as $asset) {
$parts[] = $asset->dump($additionalFilter);
}
return implode("\n", $parts);
}
/**
* getContent
*/
public function getContent(): ?string
{
return $this->content;
}
/**
* setContent
*/
public function setContent(?string $content): void
{
$this->content = $content;
}
/**
* getSourceRoot
*/
public function getSourceRoot(): ?string
{
return $this->sourceRoot;
}
/**
* getSourcePath
*/
public function getSourcePath(): ?string
{
return null;
}
/**
* getSourceDirectory returns the first available source directory, useful
* when extracting imports and a singular collection is returned
*/
public function getSourceDirectory(): ?string
{
foreach ($this as $asset) {
return $asset->getSourceDirectory();
}
return null;
}
/**
* getTargetPath
*/
public function getTargetPath(): ?string
{
return $this->targetPath;
}
/**
* setTargetPath
*/
public function setTargetPath(?string $targetPath): void
{
$this->targetPath = $targetPath;
}
/**
* getLastModified returns the highest last-modified value of all assets in the current collection.
*
* @return int|null A UNIX timestamp
*/
public function getLastModified(): ?int
{
if (!count($this->assets)) {
return null;
}
$mtime = 0;
foreach ($this as $asset) {
$assetMtime = $asset->getLastModified();
if ($assetMtime > $mtime) {
$mtime = $assetMtime;
}
}
return $mtime;
}
/**
* getIterator returns an iterator for looping recursively over unique leaves.
*/
public function getIterator(): Traversable
{
return new RecursiveIteratorIterator(new AssetCollectionFilterIterator(new AssetCollectionIterator($this, $this->clones)));
}
/**
* getVars
*/
public function getVars(): array
{
return $this->vars;
}
/**
* setValues
*/
public function setValues(array $values): void
{
$this->values = $values;
foreach ($this as $asset) {
$asset->setValues(array_intersect_key($values, array_flip($asset->getVars())));
}
}
/**
* getValues
*/
public function getValues(): array
{
return $this->values;
}
}
================================================
FILE: src/Assetic/Asset/AssetCollectionInterface.php
================================================
*/
interface AssetCollectionInterface extends AssetInterface, \Traversable
{
/**
* Returns all child assets.
*
* @return array An array of AssetInterface objects
*/
public function all(): array;
/**
* Adds an asset to the current collection.
*
* @param AssetInterface $asset An asset
*/
public function add(AssetInterface $asset): void;
/**
* Removes a leaf.
*
* @param AssetInterface $leaf The leaf to remove
* @param bool $graceful Whether the failure should return false or throw an exception
*
* @return bool Whether the asset has been found
*
* @throws \InvalidArgumentException If the asset cannot be found
*/
public function removeLeaf(AssetInterface $leaf, bool $graceful = false): bool;
/**
* Replaces an existing leaf with a new one.
*
* @param AssetInterface $needle The current asset to replace
* @param AssetInterface $replacement The new asset
* @param bool $graceful Whether the failure should return false or throw an exception
*
* @return bool Whether the asset has been found
*
* @throws \InvalidArgumentException If the asset cannot be found
*/
public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool;
}
================================================
FILE: src/Assetic/Asset/AssetInterface.php
================================================
*/
interface AssetInterface
{
/**
* Ensures the current asset includes the supplied filter.
*
* @param FilterInterface $filter A filter
*/
public function ensureFilter(FilterInterface $filter): void;
/**
* Returns an array of filters currently applied.
*
* @return array An array of filters
*/
public function getFilters(): array;
/**
* Clears all filters from the current asset.
*/
public function clearFilters(): void;
/**
* Loads the asset into memory and applies load filters.
*
* You may provide an additional filter to apply during load.
*
* @param FilterInterface $additionalFilter An additional filter
*/
public function load(?FilterInterface $additionalFilter = null): void;
/**
* Applies dump filters and returns the asset as a string.
*
* You may provide an additional filter to apply during dump.
*
* Dumping an asset should not change its state.
*
* If the current asset has not been loaded yet, it should be
* automatically loaded at this time.
*
* @param FilterInterface $additionalFilter An additional filter
*
* @return string The filtered content of the current asset
*/
public function dump(?FilterInterface $additionalFilter = null): string;
/**
* Returns the loaded content of the current asset.
*
* @return string|null The content
*/
public function getContent(): ?string;
/**
* Sets the content of the current asset.
*
* Filters can use this method to change the content of the asset.
*
* @param string|null $content The asset content
*/
public function setContent(?string $content): void;
/**
* Returns an absolute path or URL to the source asset's root directory.
*
* This value should be an absolute path to a directory in the filesystem,
* an absolute URL with no path, or null.
*
* For example:
*
* * '/path/to/web'
* * 'http://example.com'
* * null
*
* @return string|null The asset's root
*/
public function getSourceRoot(): ?string;
/**
* Returns the relative path for the source asset.
*
* This value can be combined with the asset's source root (if both are
* non-null) to get something compatible with file_get_contents().
*
* For example:
*
* * 'js/main.js'
* * 'main.js'
* * null
*
* @return string|null The source asset path
*/
public function getSourcePath(): ?string;
/**
* Returns the asset's source directory.
*
* The source directory is the directory the asset was located in
* and can be used to resolve references relative to an asset.
*
* @return string|null The asset's source directory
*/
public function getSourceDirectory(): ?string;
/**
* Returns the URL for the current asset.
*
* @return string|null A web URL where the asset will be dumped
*/
public function getTargetPath(): ?string;
/**
* Sets the URL for the current asset.
*
* @param string|null $targetPath A web URL where the asset will be dumped
*/
public function setTargetPath(?string $targetPath): void;
/**
* Returns the time the current asset was last modified.
*
* @return int|null A UNIX timestamp
*/
public function getLastModified(): ?int;
/**
* Returns an array of variable names for this asset.
*
* @return array
*/
public function getVars(): array;
/**
* Sets the values for the asset's variables.
*
* @param array $values
*/
public function setValues(array $values): void;
/**
* Returns the current values for this asset.
*
* @return array an array of strings
*/
public function getValues(): array;
}
================================================
FILE: src/Assetic/Asset/BaseAsset.php
================================================
*/
abstract class BaseAsset implements AssetInterface
{
/**
* @var FilterCollection filters
*/
protected $filters;
/**
* @var string|null sourceRoot
*/
protected $sourceRoot;
/**
* @var string|null sourcePath
*/
protected $sourcePath;
/**
* @var string|null sourceDir
*/
protected $sourceDir;
/**
* @var string|null targetPath
*/
protected $targetPath;
/**
* @var string|null content
*/
protected $content;
/**
* @var bool loaded
*/
protected $loaded;
/**
* @var array vars
*/
protected $vars;
/**
* @var array values
*/
protected $values;
/**
* __construct
*
* @param array $filters Filters for the asset
* @param string $sourceRoot The root directory
* @param string $sourcePath The asset path
* @param array $vars
*/
public function __construct(array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null, array $vars = [])
{
$this->filters = new FilterCollection($filters);
$this->sourceRoot = $sourceRoot;
$this->sourcePath = $sourcePath;
if ($sourcePath && $sourceRoot) {
$this->sourceDir = dirname("$sourceRoot/$sourcePath");
}
$this->vars = $vars;
$this->values = [];
$this->loaded = false;
}
public function __clone()
{
$this->filters = clone $this->filters;
}
public function ensureFilter(FilterInterface $filter): void
{
$this->filters->ensure($filter);
}
public function getFilters(): array
{
return $this->filters->all();
}
public function clearFilters(): void
{
$this->filters->clear();
}
/**
* Encapsulates asset loading logic.
*
* @param string $content The asset content
* @param FilterInterface $additionalFilter An additional filter
*/
protected function doLoad(?string $content, ?FilterInterface $additionalFilter = null): void
{
$filter = clone $this->filters;
if ($additionalFilter) {
$filter->ensure($additionalFilter);
}
$asset = clone $this;
$asset->setContent($content);
$filter->filterLoad($asset);
$this->content = $asset->getContent();
$this->loaded = true;
}
public function dump(?FilterInterface $additionalFilter = null): string
{
if (!$this->loaded) {
$this->load();
}
$filter = clone $this->filters;
if ($additionalFilter) {
$filter->ensure($additionalFilter);
}
$asset = clone $this;
$filter->filterDump($asset);
return $asset->getContent() ?? '';
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): void
{
$this->content = $content;
}
public function getSourceRoot(): ?string
{
return $this->sourceRoot;
}
public function getSourcePath(): ?string
{
return $this->sourcePath;
}
public function getSourceDirectory(): ?string
{
return $this->sourceDir;
}
public function getTargetPath(): ?string
{
return $this->targetPath;
}
public function setTargetPath(?string $targetPath): void
{
if ($this->vars) {
foreach ($this->vars as $var) {
if (false === strpos($targetPath, $var)) {
throw new \RuntimeException(sprintf('The asset target path "%s" must contain the variable "{%s}".', $targetPath, $var));
}
}
}
$this->targetPath = $targetPath;
}
public function getVars(): array
{
return $this->vars;
}
public function setValues(array $values): void
{
foreach ($values as $var => $v) {
if (!in_array($var, $this->vars, true)) {
throw new \InvalidArgumentException(sprintf('The asset with source path "%s" has no variable named "%s".', $this->sourcePath, $var));
}
}
$this->values = $values;
$this->loaded = false;
}
public function getValues(): array
{
return $this->values;
}
}
================================================
FILE: src/Assetic/Asset/FileAsset.php
================================================
*/
class FileAsset extends BaseAsset
{
/**
* @var string source path
*/
protected $source;
/**
* __construct.
*
* @param string $source An absolute path
* @param array $filters An array of filters
* @param string $sourceRoot The source asset root directory
* @param string $sourcePath The source asset path
* @param array $vars
*
* @throws InvalidArgumentException If the supplied root doesn't match the source when guessing the path
*/
public function __construct(string $source, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null, array $vars = [])
{
if ($sourceRoot === null) {
$sourceRoot = dirname($source);
if ($sourcePath === null) {
$sourcePath = basename($source);
}
}
elseif ($sourcePath === null) {
if (strpos($source, $sourceRoot) !== 0) {
throw new InvalidArgumentException(sprintf('The source "%s" is not in the root directory "%s"', $source, $sourceRoot));
}
$sourcePath = substr($source, strlen($sourceRoot) + 1);
}
$this->source = $source;
parent::__construct($filters, $sourceRoot, $sourcePath, $vars);
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
$source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues());
if (!is_file($source)) {
throw new RuntimeException(sprintf('The source file "%s" does not exist.', File::nicePath($source)));
}
$this->doLoad(file_get_contents($source), $additionalFilter);
}
/**
* getLastModified
*/
public function getLastModified(): ?int
{
$source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues());
if (!is_file($source)) {
throw new RuntimeException(sprintf('The source file "%s" does not exist.', File::nicePath($source)));
}
return filemtime($source);
}
}
================================================
FILE: src/Assetic/Asset/GlobAsset.php
================================================
*/
class GlobAsset extends AssetCollection
{
/**
* @var array globs
*/
protected $globs;
/**
* @var bool initialized
*/
protected $initialized;
/**
* __construct
*
* @param string|array $globs A single glob path or array of paths
* @param array $filters An array of filters
* @param string $root The root directory
* @param array $vars
*/
public function __construct($globs, array $filters = [], ?string $root = null, array $vars = [])
{
$this->globs = (array) $globs;
$this->initialized = false;
parent::__construct([], $filters, $root, $vars);
}
/**
* all
*/
public function all(): array
{
if (!$this->initialized) {
$this->initialize();
}
return parent::all();
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
if (!$this->initialized) {
$this->initialize();
}
parent::load($additionalFilter);
}
/**
* dump
*/
public function dump(?FilterInterface $additionalFilter = null): string
{
if (!$this->initialized) {
$this->initialize();
}
return parent::dump($additionalFilter);
}
/**
* getLastModified
*/
public function getLastModified(): ?int
{
if (!$this->initialized) {
$this->initialize();
}
return parent::getLastModified();
}
/**
* getIterator
*/
public function getIterator(): Traversable
{
if (!$this->initialized) {
$this->initialize();
}
return parent::getIterator();
}
/**
* setValues
*/
public function setValues(array $values): void
{
parent::setValues($values);
$this->initialized = false;
}
/**
* initialize the collection based on the glob(s) passed in.
*/
private function initialize()
{
foreach ($this->globs as $glob) {
$glob = VarUtils::resolve($glob, $this->getVars(), $this->getValues());
if (false !== $paths = glob($glob)) {
foreach ($paths as $path) {
if (is_file($path)) {
$asset = new FileAsset($path, [], $this->getSourceRoot(), null, $this->getVars());
$asset->setValues($this->getValues());
$this->add($asset);
}
}
}
}
$this->initialized = true;
}
}
================================================
FILE: src/Assetic/Asset/HttpAsset.php
================================================
*/
class HttpAsset extends BaseAsset
{
/**
* @var string sourceUrl
*/
protected $sourceUrl;
/**
* @var bool ignoreErrors
*/
protected $ignoreErrors;
/**
* __construct.
*
* @param string $sourceUrl The source URL
* @param array $filters An array of filters
* @param bool $ignoreErrors
* @param array $vars
*
* @throws InvalidArgumentException If the first argument is not an URL
*/
public function __construct(string $sourceUrl, array $filters = [], bool $ignoreErrors = false, array $vars = [])
{
if (strpos($sourceUrl, '//') === 0) {
$sourceUrl = 'http:'.$sourceUrl;
}
elseif (strpos($sourceUrl, '://') === false) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid URL.', $sourceUrl));
}
$this->sourceUrl = $sourceUrl;
$this->ignoreErrors = $ignoreErrors;
[$scheme, $url] = explode('://', $sourceUrl, 2);
[$host, $path] = explode('/', $url, 2);
parent::__construct($filters, $scheme.'://'.$host, $path, $vars);
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
$content = @file_get_contents(
VarUtils::resolve($this->sourceUrl, $this->getVars(), $this->getValues())
);
if (false === $content && !$this->ignoreErrors) {
throw new RuntimeException(sprintf('Unable to load asset from URL "%s"', $this->sourceUrl));
}
$this->doLoad($content, $additionalFilter);
}
/**
* getLastModified
*/
public function getLastModified(): ?int
{
if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(['http' => ['method' => 'HEAD']]))) {
foreach ($http_response_header as $header) {
if (stripos($header, 'Last-Modified: ') === 0) {
[, $mtime] = explode(':', $header, 2);
return strtotime(trim($mtime));
}
}
}
return null;
}
}
================================================
FILE: src/Assetic/Asset/Iterator/AssetCollectionFilterIterator.php
================================================
*/
class AssetCollectionFilterIterator extends RecursiveFilterIterator
{
protected $visited;
protected $sources;
/**
* Constructor.
*
* @param AssetCollectionIterator $iterator The inner iterator
* @param array $visited An array of visited asset objects
* @param array $sources An array of visited source strings
*/
public function __construct(AssetCollectionIterator $iterator, array $visited = [], array $sources = [])
{
parent::__construct($iterator);
$this->visited = $visited;
$this->sources = $sources;
}
/**
* Determines whether the current asset is a duplicate.
*
* De-duplication is performed based on either strict equality or by
* matching sources.
*
* @return Boolean Returns true if we have not seen this asset yet
*/
public function accept(): bool
{
$asset = $this->getInnerIterator()->current(true);
$duplicate = false;
// check strict equality
if (in_array($asset, $this->visited, true)) {
$duplicate = true;
} else {
$this->visited[] = $asset;
}
// check source
$sourceRoot = $asset->getSourceRoot();
$sourcePath = $asset->getSourcePath();
if ($sourceRoot && $sourcePath) {
$source = $sourceRoot.'/'.$sourcePath;
if (in_array($source, $this->sources)) {
$duplicate = true;
} else {
$this->sources[] = $source;
}
}
return !$duplicate;
}
/**
* Passes visited objects and source URLs to the child iterator.
*/
public function getChildren(): ?RecursiveFilterIterator
{
return new self($this->getInnerIterator()->getChildren(), $this->visited, $this->sources);
}
}
================================================
FILE: src/Assetic/Asset/Iterator/AssetCollectionIterator.php
================================================
*/
class AssetCollectionIterator implements RecursiveIterator
{
/**
* @var mixed assets
*/
protected $assets;
/**
* @var mixed filters
*/
protected $filters;
/**
* @var mixed vars
*/
protected $vars;
/**
* @var mixed output
*/
protected $output;
/**
* @var mixed clones
*/
protected $clones;
/**
* __construct
*/
public function __construct(AssetCollectionInterface $coll, SplObjectStorage $clones)
{
$this->assets = $coll->all();
$this->filters = $coll->getFilters();
$this->vars = $coll->getVars();
$this->output = $coll->getTargetPath();
$this->clones = $clones;
if (false === $pos = strrpos($this->output, '.')) {
$this->output .= '_*';
}
else {
$this->output = substr($this->output, 0, $pos).'_*'.substr($this->output, $pos);
}
}
/**
* Returns a copy of the current asset with filters and a target URL applied.
*
* @param bool $raw Returns the unmodified asset if true
*
* @return \October\Rain\Assetic\Asset\AssetInterface
*/
public function current($raw = false): mixed
{
$asset = current($this->assets);
if ($raw) {
return $asset;
}
// clone once
if (!isset($this->clones[$asset])) {
$clone = $this->clones[$asset] = clone $asset;
// generate a target path based on asset name
$name = sprintf('%s_%d', pathinfo($asset->getSourcePath(), PATHINFO_FILENAME) ?: 'part', $this->key() + 1);
$name = $this->removeDuplicateVar($name);
$clone->setTargetPath(str_replace('*', $name, $this->output));
} else {
$clone = $this->clones[$asset];
}
// cascade filters
foreach ($this->filters as $filter) {
$clone->ensureFilter($filter);
}
return $clone;
}
public function key(): mixed
{
return key($this->assets);
}
public function next(): void
{
next($this->assets);
}
public function rewind(): void
{
reset($this->assets);
}
public function valid(): bool
{
return current($this->assets) !== false;
}
public function hasChildren(): bool
{
return current($this->assets) instanceof AssetCollectionInterface;
}
/**
* @uses current()
*/
public function getChildren(): ?RecursiveIterator
{
return new self($this->current(), $this->clones);
}
private function removeDuplicateVar($name)
{
foreach ($this->vars as $var) {
$var = '{'.$var.'}';
if (false !== strpos($name, $var) && false !== strpos($this->output, $var)) {
$name = str_replace($var, '', $name);
}
}
return $name;
}
}
================================================
FILE: src/Assetic/Asset/StringAsset.php
================================================
*/
class StringAsset extends BaseAsset
{
/**
* @var string string
*/
private $string;
/**
* @var int|null lastModified
*/
private $lastModified;
/**
* __construct
*
* @param string $content The content of the asset
* @param array $filters Filters for the asset
* @param string $sourceRoot The source asset root directory
* @param string $sourcePath The source asset path
*/
public function __construct(string $content, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null)
{
$this->string = $content;
parent::__construct($filters, $sourceRoot, $sourcePath);
}
/**
* load
*/
public function load(?FilterInterface $additionalFilter = null): void
{
$this->doLoad($this->string, $additionalFilter);
}
/**
* setLastModified
*/
public function setLastModified(?int $lastModified): void
{
$this->lastModified = $lastModified;
}
/**
* getLastModified
*/
public function getLastModified(): ?int
{
return $this->lastModified;
}
}
================================================
FILE: src/Assetic/AssetManager.php
================================================
*/
class AssetManager
{
/**
* @var array assets
*/
protected $assets = [];
/**
* get an asset by name.
*
* @param string $name The asset name
* @return AssetInterface The asset
* @throws InvalidArgumentException If there is no asset by that name
*/
public function get(string $name): AssetInterface
{
if (!isset($this->assets[$name])) {
throw new InvalidArgumentException(sprintf('There is no "%s" asset.', $name));
}
return $this->assets[$name];
}
/**
* has checks if the current asset manager has a certain asset.
*
* @param string $name an asset name
* @return bool True if the asset has been set, false if not
*/
public function has(string $name): bool
{
return isset($this->assets[$name]);
}
/**
* set registers an asset to the current asset manager.
*
* @param string $name The asset name
* @param AssetInterface $asset The asset
* @throws InvalidArgumentException If the asset name is invalid
*/
public function set(string $name, AssetInterface $asset): void
{
if (!ctype_alnum(str_replace('_', '', $name))) {
throw new InvalidArgumentException(sprintf('The name "%s" is invalid.', $name));
}
$this->assets[$name] = $asset;
}
/**
* getNames returns an array of asset names.
*
* @return array An array of asset names
*/
public function getNames(): array
{
return array_keys($this->assets);
}
/**
* clear clears all assets.
*/
public function clear(): void
{
$this->assets = [];
}
}
================================================
FILE: src/Assetic/AssetWriter.php
================================================
* @author Johannes M. Schmitt
*/
class AssetWriter
{
/**
* @var string dir
*/
protected $dir;
/**
* @var array values
*/
protected $values;
/**
* __construct
*
* @param string $dir
* @param array $values
* @throws InvalidArgumentException
*/
public function __construct(string $dir, array $values = [])
{
foreach ($values as $var => $vals) {
foreach ($vals as $value) {
if (!is_string($value)) {
throw new InvalidArgumentException(sprintf('All variable values must be strings, but got %s for variable "%s".', json_encode($value), $var));
}
}
}
$this->dir = $dir;
$this->values = $values;
}
/**
* writeManagerAssets
*/
public function writeManagerAssets(AssetManager $am): void
{
foreach ($am->getNames() as $name) {
$this->writeAsset($am->get($name));
}
}
/**
* writeAsset
*/
public function writeAsset(AssetInterface $asset): void
{
foreach (VarUtils::getCombinations($asset->getVars(), $this->values) as $combination) {
$asset->setValues($combination);
static::write(
$this->dir.'/'.VarUtils::resolve(
$asset->getTargetPath(),
$asset->getVars(),
$asset->getValues()
),
$asset->dump()
);
}
}
/**
* write
*/
protected static function write(string $path, string $contents): void
{
if (!is_dir($dir = dirname($path)) && false === @mkdir($dir, 0755, true)) {
throw new RuntimeException('Unable to create directory '.$dir);
}
if (false === @file_put_contents($path, $contents)) {
throw new RuntimeException('Unable to write file '.$path);
}
}
}
================================================
FILE: src/Assetic/AsseticServiceProvider.php
================================================
app->singleton('assetic', function ($app) {
$combiner = new Combiner;
$combiner->setStoragePath(storage_path('cms/combiner/assets'));
$combiner->registerDefaultFilters();
return $combiner;
});
}
/**
* Provides the returned services.
*/
public function provides(): array
{
return [
'assetic',
];
}
}
================================================
FILE: src/Assetic/Cache/CacheInterface.php
================================================
*/
interface CacheInterface
{
/**
* Checks if the cache has a value for a key.
*
* @param string $key A unique key
*
* @return Boolean Whether the cache has a value for this key
*/
public function has($key);
/**
* Returns the value for a key.
*
* @param string $key A unique key
*
* @return string|null The value in the cache
*/
public function get($key);
/**
* Sets a value in the cache.
*
* @param string $key A unique key
* @param string $value The value to cache
*/
public function set($key, $value);
/**
* Removes a value from the cache.
*
* @param string $key A unique key
*/
public function remove($key);
}
================================================
FILE: src/Assetic/Cache/FilesystemCache.php
================================================
dir = $dir;
}
/**
* has
*/
public function has($key)
{
return file_exists($this->dir.'/'.$key);
}
/**
* get
*/
public function get($key)
{
$path = $this->dir.'/'.$key;
if (!file_exists($path)) {
throw new RuntimeException('There is no cached value for '.$key);
}
return file_get_contents($path);
}
/**
* set
*/
public function set($key, $value)
{
if (!is_dir($this->dir) && false === @mkdir($this->dir, 0755, true)) {
throw new RuntimeException('Unable to create directory '.$this->dir);
}
$path = $this->dir.'/'.$key;
if (false === @file_put_contents($path, $value)) {
throw new RuntimeException('Unable to write file '.$path);
}
File::chmod($path);
}
/**
* remove
*/
public function remove($key)
{
$path = $this->dir.'/'.$key;
if (file_exists($path) && false === @unlink($path)) {
throw new RuntimeException('Unable to remove file '.$path);
}
}
}
================================================
FILE: src/Assetic/Combiner.php
================================================
prepareCombiner($assets, $options)->dump();
}
/**
* prepareCombiner before dumping
*/
public function prepareCombiner(array $assets, array $options = []): AssetCollection
{
$targetPath = $options['targetPath'] ?? null;
$production = $options['production'] ?? false;
$useCache = $options['useCache'] ?? true;
$deepHashKey = $options['deepHashKey'] ?? null;
if ($deepHashKey !== null) {
$this->setDeepHashKeyOnFilters($deepHashKey);
}
$files = [];
$filesSalt = null;
foreach ($assets as $asset) {
$filters = $this->getFilters(File::extension($asset), (bool) $production);
$path = File::symbolizePath($asset);
if (!file_exists($path) && file_exists($this->localPath . $asset)) {
$path = $this->localPath . $asset;
}
$files[] = new FileAsset($path, $filters, base_path());
$filesSalt .= $this->localPath . $asset;
}
$filesSalt = md5($filesSalt);
$collection = new AssetCollection($files, [], $filesSalt);
$collection->setTargetPath($targetPath);
if (!$useCache || $this->storagePath === null) {
return $collection;
}
if (!File::isDirectory($this->storagePath)) {
@File::makeDirectory($this->storagePath);
}
$cache = new FilesystemCache($this->storagePath);
$cachedFiles = [];
foreach ($files as $file) {
$cachedFiles[] = new AssetCache($file, $cache);
}
$cachedCollection = new AssetCollection($cachedFiles, [], $filesSalt);
$cachedCollection->setTargetPath($targetPath);
return $cachedCollection;
}
/**
* registerDefaultFilters
*/
public function registerDefaultFilters(): void
{
// Default JavaScript filters
$this->registerFilter('js', new \October\Rain\Assetic\Filter\JavascriptImporter);
// Default StyleSheet filters
$this->registerFilter('css', new \October\Rain\Assetic\Filter\CssImportFilter);
$this->registerFilter(['css', 'less', 'scss'], new \October\Rain\Assetic\Filter\CssRewriteFilter);
$this->registerFilter('less', new \October\Rain\Assetic\Filter\LessCompiler);
$this->registerFilter('scss', new \October\Rain\Assetic\Filter\ScssCompiler);
// Production filters
$this->registerFilter('js', new \October\Rain\Assetic\Filter\JSMinFilter, true);
$this->registerFilter(['css', 'less', 'scss'], new \October\Rain\Assetic\Filter\StylesheetMinify, true);
}
/**
* setStoragePath
*/
public function setStoragePath(?string $path): void
{
$this->storagePath = $path;
}
/**
* setLocalPath
*/
public function setLocalPath(?string $path): void
{
$this->localPath = $path;
}
//
// Filters
//
/**
* registerFilter to apply to the combining process.
* @param string|array $extension Extension name. Eg: css
* @param object $filter Collection of files to combine.
* @param bool $isProduction
* @return self
*/
public function registerFilter($extension, $filter, $isProduction = false)
{
if (is_array($extension)) {
foreach ($extension as $_extension) {
$this->registerFilter($_extension, $filter);
}
return;
}
$extension = strtolower($extension);
$destination = $isProduction ? 'prodFilters' : 'filters';
if (!isset($this->$destination[$extension])) {
$this->$destination[$extension] = [];
}
if ($filter !== null) {
$this->$destination[$extension][] = $filter;
}
return $this;
}
/**
* resetFilters clears any registered filters.
* @param string $extension Extension name. Eg: css
* @return self
*/
public function resetFilters($extension = null)
{
if ($extension === null) {
$this->filters = [];
$this->prodFilters = [];
}
else {
$this->filters[$extension] = [];
$this->prodFilters[$extension] = [];
}
return $this;
}
/**
* getFilters returns all defined filters for a given extension
*/
public function getFilters(?string $extension = null, bool $isProduction = false): array
{
if ($isProduction) {
if ($extension === null) {
return array_merge($this->filters, $this->prodFilters);
}
return array_merge(
($this->filters[$extension] ?? []),
($this->prodFilters[$extension] ?? [])
);
}
if ($extension === null) {
return $this->filters;
}
return $this->filters[$extension] ?? [];
}
}
================================================
FILE: src/Assetic/Factory/AssetFactory.php
================================================
*/
class AssetFactory
{
/**
* @var mixed root
*/
protected $root;
/**
* @var mixed debug
*/
protected $debug;
/**
* @var mixed output
*/
protected $output;
/**
* @var mixed am
*/
protected $am;
/**
* @var mixed fm
*/
protected $fm;
/**
* __construct
*
* @param string $root The default root directory
* @param Boolean $debug Filters prefixed with a "?" will be omitted in debug mode
*/
public function __construct($root, $debug = false)
{
$this->root = $root ? rtrim($root, '/') : '';
$this->debug = $debug;
$this->output = 'assetic/*';
}
/**
* Sets debug mode for the current factory.
*
* @param Boolean $debug Debug mode
*/
public function setDebug($debug)
{
$this->debug = $debug;
}
/**
* Checks if the factory is in debug mode.
*
* @return Boolean Debug mode
*/
public function isDebug()
{
return $this->debug;
}
/**
* Sets the default output string.
*
* @param string $output The default output string
*/
public function setDefaultOutput($output)
{
$this->output = $output;
}
/**
* Returns the current asset manager.
*
* @return AssetManager|null The asset manager
*/
public function getAssetManager()
{
return $this->am;
}
/**
* Sets the asset manager to use when creating asset references.
*
* @param AssetManager $am The asset manager
*/
public function setAssetManager(AssetManager $am)
{
$this->am = $am;
}
/**
* Returns the current filter manager.
*
* @return FilterManager|null The filter manager
*/
public function getFilterManager()
{
return $this->fm;
}
/**
* Sets the filter manager to use when adding filters.
*
* @param FilterManager $fm The filter manager
*/
public function setFilterManager(FilterManager $fm)
{
$this->fm = $fm;
}
/**
* createAsset creates a new asset.
*
* Prefixing a filter name with a question mark will cause it to be
* omitted when the factory is in debug mode.
*
* Available options:
*
* * output: An output string
* * name: An asset name for interpolation in output patterns
* * debug: Forces debug mode on or off for this asset
* * root: An array or string of more root directories
*
* @param array|string $inputs An array of input strings
* @param array|string $filters An array of filter names
* @param array $options An array of options
*
* @return AssetCollection An asset collection
*/
public function createAsset($inputs = [], $filters = [], array $options = [])
{
if (!is_array($inputs)) {
$inputs = [$inputs];
}
if (!is_array($filters)) {
$filters = [$filters];
}
if (!isset($options['output'])) {
$options['output'] = $this->output;
}
if (!isset($options['vars'])) {
$options['vars'] = [];
}
if (!isset($options['debug'])) {
$options['debug'] = $this->debug;
}
if (!isset($options['root'])) {
$options['root'] = [$this->root];
}
else {
if (!is_array($options['root'])) {
$options['root'] = [$options['root']];
}
$options['root'][] = $this->root;
}
if (!isset($options['name'])) {
$options['name'] = $this->generateAssetName($inputs, $filters, $options);
}
$asset = $this->createAssetCollection([], $options);
$extensions = [];
// inner assets
foreach ($inputs as $input) {
if (is_array($input)) {
// nested formula
$asset->add($this->createAsset(...$input));
}
else {
$asset->add($this->parseInput($input, $options));
$extensions[pathinfo($input, PATHINFO_EXTENSION)] = true;
}
}
// filters
foreach ($filters as $filter) {
if ('?' != $filter[0]) {
$asset->ensureFilter($this->getFilter($filter));
}
elseif (!$options['debug']) {
$asset->ensureFilter($this->getFilter(substr($filter, 1)));
}
}
// append variables
if (!empty($options['vars'])) {
$toAdd = [];
foreach ($options['vars'] as $var) {
if (false !== strpos($options['output'], '{'.$var.'}')) {
continue;
}
$toAdd[] = '{'.$var.'}';
}
if ($toAdd) {
$options['output'] = str_replace('*', '*.'.implode('.', $toAdd), $options['output']);
}
}
// append consensus extension if missing
if (1 == count($extensions) && !pathinfo($options['output'], PATHINFO_EXTENSION) && $extension = key($extensions)) {
$options['output'] .= '.'.$extension;
}
// output --> target url
$asset->setTargetPath(str_replace('*', $options['name'], $options['output']));
// Return as a collection
return $asset instanceof AssetCollectionInterface
? $asset
: $this->createAssetCollection([$asset]);
}
/**
* generateAssetName
*/
public function generateAssetName($inputs, $filters, $options = []): string
{
foreach (array_diff(array_keys($options), ['output', 'debug', 'root']) as $key) {
unset($options[$key]);
}
ksort($options);
return substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7);
}
/**
* getLastModified
*/
public function getLastModified(AssetInterface $asset)
{
$mtime = 0;
foreach ($asset instanceof AssetCollectionInterface ? $asset : [$asset] as $leaf) {
$mtime = max($mtime, $leaf->getLastModified());
if (!$filters = $leaf->getFilters()) {
continue;
}
$prevFilters = [];
foreach ($filters as $filter) {
$prevFilters[] = $filter;
if (!$filter instanceof DependencyExtractorInterface) {
continue;
}
// Extract children from leaf after running all preceding filters
$clone = clone $leaf;
$clone->clearFilters();
foreach (array_slice($prevFilters, 0, -1) as $prevFilter) {
$clone->ensureFilter($prevFilter);
}
$clone->load();
foreach ($filter->getChildren($this, $clone->getContent(), $clone->getSourceDirectory()) as $child) {
$mtime = max($mtime, $this->getLastModified($child));
}
}
}
return $mtime;
}
/**
* Parses an input string string into an asset.
*
* The input string can be one of the following:
*
* * An absolute URL: If the string contains "://" or starts with "//" it will be interpreted as an HTTP asset
* * A glob: If the string contains a "*" it will be interpreted as a glob
* * A path: Otherwise the string is interpreted as a filesystem path
*
* Both globs and paths will be absolute using the current root directory.
*
* @param string $input An input string
* @param array $options An array of options
*
* @return AssetInterface An asset
*/
protected function parseInput($input, array $options = [])
{
if (false !== strpos($input, '://') || 0 === strpos($input, '//')) {
return $this->createHttpAsset($input, $options['vars']);
}
if (self::isAbsolutePath($input)) {
if ($root = self::findRootDir($input, $options['root'])) {
$path = ltrim(substr($input, strlen($root)), '/');
}
else {
$path = null;
}
}
else {
$root = $this->root;
$path = $input;
$input = $this->root.'/'.$path;
}
if (false !== strpos($input, '*')) {
return $this->createGlobAsset($input, $root, $options['vars']);
}
return $this->createFileAsset($input, $root, $path, $options['vars']);
}
/**
* createAssetCollection
*/
protected function createAssetCollection(array $assets = [], array $options = [])
{
return new AssetCollection($assets, [], null, isset($options['vars']) ? $options['vars'] : []);
}
/**
* createHttpAsset
*/
protected function createHttpAsset($sourceUrl, $vars)
{
return new HttpAsset($sourceUrl, [], false, $vars);
}
/**
* createGlobAsset
*/
protected function createGlobAsset($glob, $root = null, $vars = [])
{
return new GlobAsset($glob, [], $root, $vars);
}
/**
* createFileAsset
*/
protected function createFileAsset($source, $root = null, $path = null, $vars = [])
{
return new FileAsset($source, [], $root, $path, $vars);
}
/**
* getFilter
*/
protected function getFilter($name)
{
if (!$this->fm) {
throw new LogicException('There is no filter manager.');
}
return $this->fm->get($name);
}
/**
* isAbsolutePath
*/
private static function isAbsolutePath($path)
{
return '/' == $path[0] || '\\' == $path[0] || (3 < strlen($path) && ctype_alpha($path[0]) && $path[1] == ':' && ('\\' == $path[2] || '/' == $path[2]));
}
/**
* findRootDir loops through the root directories and returns the first match.
*
* @param string $path An absolute path
* @param array $roots An array of root directories
*
* @return string|null The matching root directory, if found
*/
private static function findRootDir($path, array $roots)
{
foreach ($roots as $root) {
if (0 === strpos($path, $root)) {
return $root;
}
}
}
}
================================================
FILE: src/Assetic/Filter/BaseCssFilter.php
================================================
*/
abstract class BaseCssFilter implements FilterInterface
{
/**
* @see CssUtils::filterReferences()
*/
protected function filterReferences(string $content, callable $callback): string
{
return CssUtils::filterReferences($content, $callback);
}
/**
* @see CssUtils::filterUrls()
*/
protected function filterUrls(string $content, callable $callback): string
{
return CssUtils::filterUrls($content, $callback);
}
/**
* @see CssUtils::filterImports()
*/
protected function filterImports(string $content, callable $callback, bool $includeUrl = true): string
{
return CssUtils::filterImports($content, $callback, $includeUrl);
}
/**
* @see CssUtils::filterIEFilters()
*/
protected function filterIEFilters(string $content, callable $callback): string
{
return CssUtils::filterIEFilters($content, $callback);
}
}
================================================
FILE: src/Assetic/Filter/CssImportFilter.php
================================================
*/
class CssImportFilter extends BaseCssFilter implements HashableInterface, DependencyExtractorInterface
{
/**
* @var FilterInterface|null importFilter
*/
protected $importFilter;
/**
* @var string|null lastHash
*/
protected $lastHash;
/**
* __construct
*
* @param FilterInterface $importFilter Filter for each imported asset
*/
public function __construct(?FilterInterface $importFilter = null)
{
$this->importFilter = $importFilter ?: new CssRewriteFilter();
}
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
$importFilter = $this->importFilter;
$sourceRoot = $asset->getSourceRoot();
$sourcePath = $asset->getSourcePath();
$callback = function ($matches) use ($importFilter, $sourceRoot, $sourcePath) {
if (!$matches['url'] || $sourceRoot === null) {
return $matches[0];
}
$importRoot = $sourceRoot;
// Absolute
if (strpos($matches['url'], '://') !== false) {
[$importScheme, $tmp] = explode('://', $matches['url'], 2);
[$importHost, $importPath] = explode('/', $tmp, 2);
$importRoot = $importScheme.'://'.$importHost;
}
// Protocol-relative
elseif (strpos($matches['url'], '//') === 0) {
[$importHost, $importPath] = explode('/', substr($matches['url'], 2), 2);
$importRoot = '//'.$importHost;
}
// Root-relative
elseif ($matches['url'][0] == '/') {
$importPath = substr($matches['url'], 1);
}
// Document-relative
elseif ($sourcePath !== null) {
$importPath = $matches['url'];
if ('.' != $sourceDir = dirname($sourcePath)) {
$importPath = $sourceDir.'/'.$importPath;
}
}
else {
return $matches[0];
}
$importSource = $importRoot.'/'.$importPath;
if (strpos($importSource, '://') !== false || strpos($importSource, '//') === 0) {
$import = new HttpAsset($importSource, [$importFilter], true);
}
// Ignore non-css and non-existent imports
elseif (pathinfo($importPath, PATHINFO_EXTENSION) != 'css' || !file_exists($importSource)) {
return $matches[0];
}
else {
$import = new FileAsset($importSource, [$importFilter], $importRoot, $importPath);
}
$import->setTargetPath($sourcePath);
return $import->dump();
};
$content = $asset->getContent();
$lastHash = md5($content);
do {
$content = $this->filterImports($content, $callback);
$hash = md5($content);
} while ($lastHash != $hash && ($lastHash = $hash));
$asset->setContent($content);
}
/**
* filterDump
*/
public function filterDump(AssetInterface $asset): void
{
}
/**
* hashAsset
*/
public function hashAsset($asset, $localPath)
{
$factory = new AssetFactory($localPath);
$children = $this->getAllChildren($factory, file_get_contents($asset), dirname($asset));
$allFiles = [];
foreach ($children as $child) {
$allFiles[] = $child;
}
$modified = [];
foreach ($allFiles as $file) {
$modified[] = $file->getLastModified();
}
return md5(implode('|', $modified));
}
/**
* setHash
*/
public function setHash($hash)
{
$this->lastHash = $hash;
}
/**
* hash generated for the object
* @return string
*/
public function hash()
{
return $this->lastHash ?: serialize($this);
}
/**
* getAllChildren loads all children recursively
*/
public function getAllChildren(AssetFactory $factory, $content, $loadPath = null)
{
$children = (new static)->getChildren($factory, $content, $loadPath);
foreach ($children as $child) {
$childContent = file_get_contents($child->getSourceRoot().'/'.$child->getSourcePath());
$children = array_merge($children, (new static)->getChildren($factory, $childContent, $loadPath.'/'.dirname($child->getSourcePath())));
}
return $children;
}
/**
* getChildren only returns one level of children
*/
public function getChildren(AssetFactory $factory, $content, $loadPath = null)
{
if (!$loadPath) {
return [];
}
$children = [];
foreach (CssUtils::extractImports($content) as $reference) {
// Strict check, only allow .css imports
if (substr($reference, -4) !== '.css') {
continue;
}
if (file_exists($file = $loadPath.'/'.$reference)) {
$coll = $factory->createAsset($file, [], ['root' => $loadPath]);
foreach ($coll as $leaf) {
$leaf->ensureFilter($this);
$children[] = $leaf;
break;
}
}
}
return $children;
}
}
================================================
FILE: src/Assetic/Filter/CssMinFilter.php
================================================
*/
class CssMinFilter implements FilterInterface
{
private $filters;
private $plugins;
public function __construct()
{
$this->filters = [];
$this->plugins = [];
}
public function setFilters(array $filters)
{
$this->filters = $filters;
}
public function setFilter($name, $value)
{
$this->filters[$name] = $value;
}
public function setPlugins(array $plugins)
{
$this->plugins = $plugins;
}
public function setPlugin($name, $value)
{
$this->plugins[$name] = $value;
}
public function filterLoad(AssetInterface $asset): void
{
}
public function filterDump(AssetInterface $asset): void
{
$filters = $this->filters;
$plugins = $this->plugins;
if (isset($filters['ImportImports']) && true === $filters['ImportImports']) {
if ($dir = $asset->getSourceDirectory()) {
$filters['ImportImports'] = array('BasePath' => $dir);
} else {
unset($filters['ImportImports']);
}
}
$asset->setContent(\CssMin::minify($asset->getContent(), $filters, $plugins));
}
}
================================================
FILE: src/Assetic/Filter/CssRewriteFilter.php
================================================
*/
class CssRewriteFilter extends BaseCssFilter
{
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
}
/**
* filterDump
*/
public function filterDump(AssetInterface $asset): void
{
$sourceBase = $asset->getSourceRoot();
$sourcePath = $asset->getSourcePath();
$targetPath = $asset->getTargetPath();
if ($sourcePath === null || $targetPath === null || $sourcePath == $targetPath) {
return;
}
// Learn how to get from the target back to the source
if (strpos($sourceBase, '://') !== false) {
[$scheme, $url] = explode('://', $sourceBase.'/'.$sourcePath, 2);
[$host, $path] = explode('/', $url, 2);
$host = $scheme.'://'.$host.'/';
$path = false === strpos($path, '/') ? '' : dirname($path);
$path .= '/';
}
else {
// Assume source and target are on the same host
$host = '';
// Pop entries off the target until it fits in the source
if (dirname($sourcePath) == '.') {
$path = str_repeat('../', substr_count($targetPath, '/'));
}
elseif (($targetDir = dirname($targetPath)) == '.') {
$path = dirname($sourcePath).'/';
}
else {
$path = '';
while (strpos($sourcePath, $targetDir) !== 0) {
if (($pos = strrpos($targetDir, '/')) !== false) {
$targetDir = substr($targetDir, 0, $pos);
$path .= '../';
}
else {
$targetDir = '';
$path .= '../';
break;
}
}
$path .= ltrim(substr(dirname($sourcePath).'/', strlen($targetDir)), '/');
}
}
$content = $this->filterReferences($asset->getContent(), function ($matches) use ($host, $path) {
// Absolute or protocol-relative or data uri
if (
strpos($matches['url'], '://') !== false ||
strpos($matches['url'], '#') === 0 ||
strpos($matches['url'], '//') === 0 ||
strpos($matches['url'], 'data:') === 0
) {
return $matches[0];
}
// Root relative
if (isset($matches['url'][0]) && '/' == $matches['url'][0]) {
return str_replace($matches['url'], $host.$matches['url'], $matches[0]);
}
// Document relative
$url = $matches['url'];
while (strpos($url, '../') === 0 && substr_count($path, '/') >= 2) {
$path = substr($path, 0, strrpos(rtrim($path, '/'), '/') + 1);
$url = substr($url, 3);
}
$parts = [];
foreach (explode('/', $host.$path.$url) as $part) {
if ($part === '..' && count($parts) && end($parts) !== '..') {
array_pop($parts);
}
else {
$parts[] = $part;
}
}
return str_replace($matches['url'], implode('/', $parts), $matches[0]);
});
$asset->setContent($content);
}
}
================================================
FILE: src/Assetic/Filter/DependencyExtractorInterface.php
================================================
*/
interface DependencyExtractorInterface extends FilterInterface
{
/**
* Returns child assets.
*
* @param AssetFactory $factory The asset factory
* @param string $content The asset content
* @param string $loadPath An optional load path
*
* @return AssetInterface[] Child assets
*/
public function getChildren(AssetFactory $factory, $content, $loadPath = null);
}
================================================
FILE: src/Assetic/Filter/FilterCollection.php
================================================
*/
class FilterCollection implements FilterInterface, \IteratorAggregate, \Countable
{
/**
* @var array filters
*/
protected $filters = [];
/**
* __construct
*/
public function __construct(array $filters = [])
{
foreach ($filters as $filter) {
$this->ensure($filter);
}
}
/**
* Checks that the current collection contains the supplied filter.
*
* If the supplied filter is another filter collection, each of its
* filters will be checked.
*/
public function ensure(FilterInterface $filter): void
{
if ($filter instanceof \Traversable) {
foreach ($filter as $f) {
$this->ensure($f);
}
} elseif (!in_array($filter, $this->filters, true)) {
$this->filters[] = $filter;
}
}
/**
* all
*/
public function all(): array
{
return $this->filters;
}
/**
* clear
*/
public function clear(): void
{
$this->filters = [];
}
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
foreach ($this->filters as $filter) {
$filter->filterLoad($asset);
}
}
/**
* filterDump
*/
public function filterDump(AssetInterface $asset): void
{
foreach ($this->filters as $filter) {
$filter->filterDump($asset);
}
}
/**
* getIterator
*/
public function getIterator(): Traversable
{
return new \ArrayIterator($this->filters);
}
/**
* count
*/
public function count(): int
{
return count($this->filters);
}
}
================================================
FILE: src/Assetic/Filter/FilterInterface.php
================================================
*/
interface FilterInterface
{
/**
* Filters an asset after it has been loaded.
*
* @param AssetInterface $asset An asset
*/
public function filterLoad(AssetInterface $asset): void;
/**
* Filters an asset just before it's dumped.
*
* @param AssetInterface $asset An asset
*/
public function filterDump(AssetInterface $asset): void;
}
================================================
FILE: src/Assetic/Filter/HashableInterface.php
================================================
*/
interface HashableInterface
{
/**
* Generates a hash for the object
*
* @return string Object hash
*/
public function hash();
}
================================================
FILE: src/Assetic/Filter/JSMinFilter.php
================================================
*/
class JSMinFilter implements FilterInterface
{
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
}
/**
* filterDump will use JSMin to minify the asset and checks the filename
* for "min.js" to issues arising from double minification.
*/
public function filterDump(AssetInterface $asset): void
{
$contents = $asset->getContent();
$isMinifiedAlready = strpos($asset->getSourcePath(), '.min.js') !== false;
if (!$isMinifiedAlready) {
$contents = JSMin::minify($contents);
}
$asset->setContent($contents);
}
}
================================================
FILE: src/Assetic/Filter/JSqueezeFilter.php
================================================
*/
class JSqueezeFilter implements FilterInterface
{
/**
* @var mixed singleLine
*/
protected $singleLine = true;
/**
* @var bool keepImportantComments
*/
protected $keepImportantComments = true;
/**
* @var mixed className
*/
protected $className;
/**
* @var bool specialVarRx
*/
protected $specialVarRx = false;
/**
* @var mixed defaultRx
*/
protected $defaultRx;
/**
* __construct
*/
public function __construct()
{
// JSqueeze is namespaced since 2.x, this works with both 1.x and 2.x
if (class_exists('\\Patchwork\\JSqueeze')) {
$this->className = '\\Patchwork\\JSqueeze';
$this->defaultRx = \Patchwork\JSqueeze::SPECIAL_VAR_PACKER;
} else {
$this->className = '\\JSqueeze';
$this->defaultRx = \JSqueeze::SPECIAL_VAR_RX;
}
}
public function setSingleLine($bool)
{
$this->singleLine = (bool) $bool;
}
// call setSpecialVarRx(true) to enable global var/method/property
// renaming with the default regex (for 1.x or 2.x)
public function setSpecialVarRx($specialVarRx)
{
if (true === $specialVarRx) {
$this->specialVarRx = $this->defaultRx;
} else {
$this->specialVarRx = $specialVarRx;
}
}
public function keepImportantComments($bool)
{
$this->keepImportantComments = (bool) $bool;
}
public function filterLoad(AssetInterface $asset): void
{
}
public function filterDump(AssetInterface $asset): void
{
$parser = new $this->className();
$asset->setContent($parser->squeeze(
$asset->getContent(),
$this->singleLine,
$this->keepImportantComments,
$this->specialVarRx
));
}
}
================================================
FILE: src/Assetic/Filter/JavascriptImporter.php
================================================
scriptPath = dirname($asset->getSourceRoot() . '/' . $asset->getSourcePath());
$this->scriptFile = basename($asset->getSourcePath());
$asset->setContent($this->parse($asset->getContent()));
}
/**
* Process JS imports inside a string of javascript
* @param string $content JS code to process.
* @return string Processed JS.
*/
protected function parse(string $content): string
{
$macros = [];
$imported = '';
// Look for: /* comments */
if (!preg_match_all('@/\*(.*)\*/@msU', $content, $matches)) {
return $content;
}
foreach ($matches[1] as $macro) {
// Look for: =include something
if (!preg_match_all('/=([^\\s]*)\\s(.*)\n/', $macro, $matches2)) {
continue;
}
foreach ($matches2[1] as $index => $macroName) {
$method = 'directive' . ucfirst(strtolower($macroName));
if (method_exists($this, $method)) {
$imported .= $this->$method($matches2[2][$index]);
}
}
}
return $imported . $content;
}
/**
* Directive to process script includes
*/
protected function directiveInclude(string $data, bool $required = false): string
{
$require = explode(',', $data);
$result = "";
foreach ($require as $script) {
$script = trim($script);
if (!File::extension($script)) {
$script = $script . '.js';
}
$scriptPath = realpath($this->scriptPath . '/' . $script);
if (!File::isFile($scriptPath)) {
$errorMsg = sprintf("File '%s' not found. in %s", $script, $this->scriptFile);
if ($required) {
throw new RuntimeException($errorMsg);
}
$result .= '/* ' . $errorMsg . ' */' . PHP_EOL;
continue;
}
/*
* Exclude duplicates
*/
if (in_array($script, $this->includedFiles)) {
continue;
}
$this->includedFiles[] = $script;
/*
* Nested parsing
*/
$oldScriptPath = $this->scriptPath;
$oldScriptFile = $this->scriptFile;
$this->scriptPath = dirname($scriptPath);
$this->scriptFile = basename($scriptPath);
$content = File::get($scriptPath);
$content = $this->parse($content) . PHP_EOL;
$this->scriptPath = $oldScriptPath;
$this->scriptFile = $oldScriptFile;
/*
* Parse in "magic constants"
*/
$content = str_replace(
['__DATE__', '__FILE__'],
[date("D M j G:i:s T Y"), $script],
$content
);
$result .= $content;
}
return $result;
}
/**
* Directive to process mandatory script includes
*/
protected function directiveRequire(string $data): string
{
return $this->directiveInclude($data, true);
}
/**
* Directive to define and replace variables
*/
protected function directiveDefine(string $data): string
{
if (preg_match('@([^\\s]*)\\s+(.*)@', $data, $matches)) {
// str_replace($matches[1], $matches[2], $context);
$this->definedVars[] = [$matches[1], $matches[2]];
}
return '';
}
}
================================================
FILE: src/Assetic/Filter/LessCompiler.php
================================================
presets = $presets;
}
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
$parser = new Less_Parser();
// CSS Rewriter will take care of this
$parser->SetOption('relativeUrls', false);
$parser->parseFile($asset->getSourceRoot() . '/' . $asset->getSourcePath());
// Set the LESS variables after parsing to override them
$parser->ModifyVars($this->presets);
$asset->setContent($parser->getCss());
}
/**
* filterDump
*/
public function filterDump(AssetInterface $asset): void
{
}
/**
* hashAsset
*/
public function hashAsset($asset, $localPath)
{
$factory = new AssetFactory($localPath);
$children = $this->getChildren($factory, file_get_contents($asset), dirname($asset));
$allFiles = [];
foreach ($children as $child) {
$allFiles[] = $child;
}
$modified = [];
foreach ($allFiles as $file) {
$modified[] = $file->getLastModified();
}
return md5(implode('|', $modified));
}
/**
* setHash
*/
public function setHash($hash)
{
$this->lastHash = $hash;
}
/**
* hash generated for the object
* @return string
*/
public function hash()
{
return $this->lastHash ?: serialize($this);
}
/**
* getChildren loads children recursively
*/
public function getChildren(AssetFactory $factory, $content, $loadPath = null)
{
$children = (new LessphpFilter)->getChildren($factory, $content, $loadPath);
foreach ($children as $child) {
$childContent = file_get_contents($child->getSourceRoot().'/'.$child->getSourcePath());
$children = array_merge($children, (new LessphpFilter)->getChildren($factory, $childContent, $loadPath.'/'.dirname($child->getSourcePath())));
}
return $children;
}
}
================================================
FILE: src/Assetic/Filter/LessphpFilter.php
================================================
* @author Kris Wallsmith
*/
class LessphpFilter implements DependencyExtractorInterface
{
private $presets = [];
private $formatter;
private $preserveComments;
private $customFunctions = [];
private $options = [];
/**
* Lessphp Load Paths
*
* @var array
*/
protected $loadPaths = [];
/**
* Adds a load path to the paths used by lessphp
*
* @param string $path Load Path
*/
public function addLoadPath($path)
{
$this->loadPaths[] = $path;
}
/**
* Sets load paths used by lessphp
*
* @param array $loadPaths Load paths
*/
public function setLoadPaths(array $loadPaths)
{
$this->loadPaths = $loadPaths;
}
public function setPresets(array $presets)
{
$this->presets = $presets;
}
public function setOptions(array $options)
{
$this->options = $options;
}
/**
* @param string $formatter One of "lessjs", "compressed", or "classic".
*/
public function setFormatter($formatter)
{
$this->formatter = $formatter;
}
/**
* @param boolean $preserveComments
*/
public function setPreserveComments($preserveComments)
{
$this->preserveComments = $preserveComments;
}
public function filterLoad(AssetInterface $asset): void
{
$lc = new \lessc();
if ($dir = $asset->getSourceDirectory()) {
$lc->importDir = $dir;
}
foreach ($this->loadPaths as $loadPath) {
$lc->addImportDir($loadPath);
}
foreach ($this->customFunctions as $name => $callable) {
$lc->registerFunction($name, $callable);
}
if ($this->formatter) {
$lc->setFormatter($this->formatter);
}
if (null !== $this->preserveComments) {
$lc->setPreserveComments($this->preserveComments);
}
if (method_exists($lc, 'setOptions') && count($this->options) > 0) {
$lc->setOptions($this->options);
}
$asset->setContent($lc->parse($asset->getContent(), $this->presets));
}
public function registerFunction($name, $callable)
{
$this->customFunctions[$name] = $callable;
}
public function filterDump(AssetInterface $asset): void
{
}
public function getChildren(AssetFactory $factory, $content, $loadPath = null)
{
$loadPaths = $this->loadPaths;
if ($loadPath !== null) {
$loadPaths[] = $loadPath;
}
if (empty($loadPaths)) {
return [];
}
$children = [];
foreach (LessUtils::extractImports($content) as $reference) {
if (substr($reference, -4) === '.css') {
// skip normal css imports
// todo: skip imports with media queries
continue;
}
if (substr($reference, -5) !== '.less') {
$reference .= '.less';
}
foreach ($loadPaths as $loadPath) {
if (file_exists($file = $loadPath.'/'.$reference)) {
$coll = $factory->createAsset($file, [], ['root' => $loadPath]);
foreach ($coll as $leaf) {
$leaf->ensureFilter($this);
$children[] = $leaf;
break 2;
}
}
}
}
return $children;
}
}
================================================
FILE: src/Assetic/Filter/ScssCompiler.php
================================================
currentFiles[] = $asset;
}
}
});
}
public function setPresets(array $presets)
{
$this->variables = array_merge($this->variables, $presets);
}
public function setVariables(array $variables)
{
$this->variables = array_merge($this->variables, $variables);
}
public function addVariable($variable)
{
$this->variables[] = $variable;
}
public function filterLoad(AssetInterface $asset): void
{
parent::setVariables($this->variables);
parent::filterLoad($asset);
}
public function setHash($hash)
{
$this->lastHash = $hash;
}
/**
* Generates a hash for the object
* @return string
*/
public function hash()
{
return $this->lastHash ?: serialize($this);
}
public function hashAsset($asset, $localPath)
{
$factory = new AssetFactory($localPath);
$children = $this->getChildren($factory, file_get_contents($asset), dirname($asset));
$allFiles = [];
foreach ($children as $child) {
$allFiles[] = $child;
}
$modifieds = [];
foreach ($allFiles as $file) {
$modifieds[] = $file->getLastModified();
}
return md5(implode('|', $modifieds));
}
}
================================================
FILE: src/Assetic/Filter/ScssphpFilter.php
================================================
*/
class ScssphpFilter implements DependencyExtractorInterface
{
/**
* @var array importPaths
*/
protected $importPaths = [];
/**
* @var array customFunctions
*/
protected $customFunctions = [];
/**
* @var mixed formatter
*/
protected $formatter;
/**
* @var array variables
*/
protected $variables = [];
/**
* setFormatter
*/
public function setFormatter($formatter)
{
$this->formatter = $formatter;
}
/**
* setVariables
*/
public function setVariables(array $variables)
{
$this->variables = $variables;
}
/**
* addVariable
*/
public function addVariable($variable)
{
$this->variables[] = $variable;
}
/**
* setImportPaths
*/
public function setImportPaths(array $paths)
{
$this->importPaths = $paths;
}
/**
* addImportPath
*/
public function addImportPath($path)
{
$this->importPaths[] = $path;
}
/**
* registerFunction
*/
public function registerFunction($name, $callable)
{
$this->customFunctions[$name] = $callable;
}
/**
* filterLoad
*/
public function filterLoad(AssetInterface $asset): void
{
$sc = new Compiler();
if ($dir = $asset->getSourceDirectory()) {
$sc->addImportPath($dir);
}
foreach ($this->importPaths as $path) {
$sc->addImportPath($path);
}
foreach ($this->customFunctions as $name => $callable) {
$sc->registerFunction($name, $callable);
}
if ($this->formatter) {
$sc->setOutputStyle($this->formatter);
}
if (!empty($this->variables)) {
$sc->addVariables($this->variables);
}
// Generate source map file
$useSourceMaps = Config::get('cms.enable_asset_source_maps', false);
if ($useSourceMaps) {
$mapFile = md5($asset->getSourcePath()).'.css.map';
$sc->setSourceMap(Compiler::SOURCE_MAP_FILE);
$sc->setSourceMapOptions([
'sourceMapURL' => $this->getSourceMapPublicUrl().'/'.$mapFile,
'sourceMapBasepath' => '',
'sourceRoot' => '/',
]);
$result = $sc->compileString($asset->getContent());
File::put($this->getSourceMapLocalPath().'/'.$mapFile, $result->getSourceMap());
}
else {
$result = $sc->compileString($asset->getContent());
}
$asset->setContent($result->getCss());
}
/**
* filterDump
*/
public function filterDump(AssetInterface $asset): void
{
}
/**
* getChildren
*/
public function getChildren(AssetFactory $factory, $content, $loadPath = null)
{
$sc = new Compiler();
if ($loadPath !== null) {
$sc->addImportPath($loadPath);
}
foreach ($this->importPaths as $path) {
$sc->addImportPath($path);
}
$children = [];
foreach (SassUtils::extractImports($content) as $match) {
$file = $sc->findImport($match);
if ($file) {
$children[] = $child = $factory->createAsset($file, [], ['root' => $loadPath]);
$child->load();
$children = array_merge(
$children,
$this->getChildren($factory, $child->getContent(), $child->getSourceDirectory())
);
}
}
return $children;
}
/**
* getSourceMapLocalPath returns the local path for source maps
*/
protected function getSourceMapLocalPath(): string
{
$path = rtrim(Config::get('filesystems.disks.resources.root', storage_path('app/resources')), '/');
$path .= '/sourcemap';
if (!File::isDirectory($path)) {
File::makeDirectory($path, 0755, true, true);
}
return $path;
}
/**
* getSourceMapPublicUrl returns the public address for the source map path
*/
protected function getSourceMapPublicUrl(): string
{
$fullPath = Config::get('filesystems.disks.resources.url', '/storage/app/resources');
$fullPath .= '/sourcemap';
if (
Config::get('filesystems.disks.resources.driver') === 'local' &&
Config::get('system.relative_links') === true
) {
return Url::toRelative($fullPath);
}
return Url::asset($fullPath);
}
}
================================================
FILE: src/Assetic/Filter/StylesheetMinify.php
================================================
setContent($this->minify($asset->getContent()));
}
/**
* Minify CSS
* @param string $css CSS code to minify.
* @return string Minified CSS.
*/
protected function minify(string $css): string
{
// Normalize whitespace in a smart way
$css = preg_replace('/\s{2,}/', ' ', $css);
// Remove spaces before and after comment
$css = preg_replace('/(\s+)(\/\*[^!](.*?)\*\/)(\s+)/', '$2', $css);
// Add a space after retained /*! comments to prevent valid CSS from appearing as a
// removable comment, eg: /*! keepme */*,:after{content:'nuked'}/*another comment*/
$css = preg_replace('#(/\*!.*?\*/)(\*)#s', '$1 $2', $css);
// Remove comment blocks, everything between /* and */, ignore /*! comments
$css = preg_replace('#/\*[^\!].*?\*/#s', '', $css);
// Remove ; before }
$css = preg_replace('/;(?=\s*})/', '', $css);
// Remove space after , : ; { } */ >, but not after !*/
$css = preg_replace('/(,|:|;|\{|}|[^!]\*\/|>) /', '$1', $css);
// Remove space before , ; { } >
$css = preg_replace('/ (,|;|\{|}|>)/', '$1', $css);
// Remove newline before } >
$css = preg_replace('/(\r\n|\r|\n)(})/', '$2', $css);
// Remove trailing zeros from float numbers preceded by : or a white-space
// -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
$css = preg_replace('/((?
*/
class FilterManager
{
/**
* @var array filters
*/
protected $filters = [];
/**
* set
*/
public function set(string $alias, FilterInterface $filter): void
{
$this->checkName($alias);
$this->filters[$alias] = $filter;
}
/**
* get
*/
public function get(string $alias): FilterInterface
{
if (!isset($this->filters[$alias])) {
throw new InvalidArgumentException(sprintf('There is no "%s" filter.', $alias));
}
return $this->filters[$alias];
}
/**
* has
*/
public function has(string $alias): bool
{
return isset($this->filters[$alias]);
}
/**
* getNames
*/
public function getNames(): array
{
return array_keys($this->filters);
}
/**
* Checks that a name is valid.
* @param string $name An asset name candidate
* @throws InvalidArgumentException If the asset name is invalid
*/
protected function checkName(string $name): void
{
if (!ctype_alnum(str_replace('_', '', $name))) {
throw new InvalidArgumentException(sprintf('The name "%s" is invalid.', $name));
}
}
}
================================================
FILE: src/Assetic/README.md
================================================
# Rain Assetic Resources
Assetic is a simple library that lets you compile and combine basic LESS and SCSS files.
## Basic usage
You may use the `parse` methods to parse LESS or SCSS respectively, the first argument is the asset paths and the second argument is the options. The file extension determines which compiler is used, either `.less` or `.scss`.
```php
$combiner = new October\Rain\Assetic\Combiner;
echo $combiner->parse([
'/path/to/src/styles.less',
'/path/to/src/theme.less'
], [
'production' => true
]);
```
The following options are supported
Options | Usage
------- | ---------
`production` | Combine with production filters (eg: minification).
`targetPath` | Sets the target output path for rewriting asset locations.
`useCache` | Use a file based cache to speed up performance.
`deepHashKey` | Cache key used for busting deep hashing.
================================================
FILE: src/Assetic/Traits/HasDeepHasher.php
================================================
localPath);
return $factory->getLastModified($combiner);
}
/**
* getDeepHashFromAssets returns a deep hash on filters that support it.
* @param array $assets List of asset files.
* @return string
*/
public function getDeepHashFromAssets($assets)
{
$key = '';
$assetFiles = [];
foreach ($assets as $file) {
$path = File::symbolizePath($file);
if (file_exists($path)) {
$assetFiles[] = $path;
}
elseif (file_exists($this->localPath . $path)) {
$assetFiles[] = $this->localPath . $path;
}
}
foreach ($assetFiles as $file) {
$filters = $this->getFilters(File::extension($file));
foreach ($filters as $filter) {
if (method_exists($filter, 'hashAsset')) {
$key .= $filter->hashAsset($file, $this->localPath);
}
}
}
return $key;
}
/**
* setHashOnCombinerFilters busts the cache based on a different cache key.
*/
protected function setDeepHashKeyOnFilters($hash)
{
$allFilters = array_merge(...array_values($this->getFilters()));
foreach ($allFilters as $filter) {
if (method_exists($filter, 'setHash')) {
$filter->setHash($hash);
}
}
}
}
================================================
FILE: src/Assetic/Util/CssUtils.php
================================================
*/
abstract class CssUtils
{
const REGEX_URLS = '/url\((["\']?)(?P.*?)(\\1)\)/';
const REGEX_IMPORTS = '/@import (?:url\()?(\'|"|)(?P[^\'"\)\n\r]*)\1\)?;?/';
const REGEX_IMPORTS_NO_URLS = '/@import (?!url\()(\'|"|)(?P[^\'"\)\n\r]*)\1;?/';
const REGEX_IE_FILTERS = '/src=(["\']?)(?P.*?)\\1/';
const REGEX_COMMENTS = '/(\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)/';
/**
* Filters all references -- url() and "@import" -- through a callable.
*
* @param string $content The CSS
* @param callable $callback A PHP callable
*
* @return string The filtered CSS
*/
public static function filterReferences(string $content, callable $callback): string
{
$content = static::filterUrls($content, $callback);
$content = static::filterImports($content, $callback, false);
$content = static::filterIEFilters($content, $callback);
return $content;
}
/**
* Filters all CSS url()'s through a callable.
*
* @param string $content The CSS
* @param callable $callback A PHP callable
*
* @return string The filtered CSS
*/
public static function filterUrls(string $content, callable $callback): string
{
$pattern = static::REGEX_URLS;
return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {
return preg_replace_callback($pattern, $callback, $part);
});
}
/**
* Filters all CSS imports through a callable.
*
* @param string $content The CSS
* @param callable $callback A PHP callable
* @param Boolean $includeUrl Whether to include url() in the pattern
*
* @return string The filtered CSS
*/
public static function filterImports(string $content, callable $callback, bool $includeUrl = true): string
{
$pattern = $includeUrl ? static::REGEX_IMPORTS : static::REGEX_IMPORTS_NO_URLS;
return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {
return preg_replace_callback($pattern, $callback, $part);
});
}
/**
* Filters all IE filters (AlphaImageLoader filter) through a callable.
*
* @param string $content The CSS
* @param callable $callback A PHP callable
*
* @return string The filtered CSS
*/
public static function filterIEFilters(string $content, callable $callback): string
{
$pattern = static::REGEX_IE_FILTERS;
return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {
return preg_replace_callback($pattern, $callback, $part);
});
}
/**
* Filters each non-comment part through a callable.
*
* @param string $content The CSS
* @param callable $callback A PHP callable
*
* @return string The filtered CSS
*/
public static function filterCommentless(string $content, callable $callback): string
{
$result = '';
foreach (preg_split(static::REGEX_COMMENTS, $content, -1, PREG_SPLIT_DELIM_CAPTURE) as $part) {
if (!preg_match(static::REGEX_COMMENTS, $part, $match) || $part != $match[0]) {
$part = $callback($part);
}
$result .= $part;
}
return $result;
}
/**
* Extracts all references from the supplied CSS content.
*
* @param string $content The CSS content
*
* @return array An array of unique URLs
*/
public static function extractImports(string $content): array
{
$imports = [];
static::filterImports($content, function ($matches) use (&$imports) {
$imports[] = $matches['url'];
});
return array_unique(array_filter($imports));
}
final private function __construct()
{
}
}
================================================
FILE: src/Assetic/Util/LessUtils.php
================================================
*/
abstract class LessUtils extends CssUtils
{
const REGEX_IMPORTS = '/@import(?:-once)? (?:\([a-z]*\) )?(?:url\()?(\'|"|)(?P[^\'"\)\n\r]*)\1\)?;?/';
const REGEX_IMPORTS_NO_URLS = '/@import(?:-once)? (?:\([a-z]*\) )?(?!url\()(\'|"|)(?P[^\'"\)\n\r]*)\1;?/';
const REGEX_COMMENTS = '/((?:\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)|\/\/[^\n]+)/';
}
================================================
FILE: src/Assetic/Util/SassUtils.php
================================================
*/
abstract class SassUtils extends CssUtils
{
const REGEX_COMMENTS = '/((?:\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)|\/\/[^\n]+)/';
}
================================================
FILE: src/Assetic/Util/VarUtils.php
================================================
*/
abstract class VarUtils
{
/**
* Resolves variable placeholders.
*
* @param string $template A template string
* @param array $vars Variable names
* @param array $values Variable values
*
* @return string The resolved string
*
* @throws \InvalidArgumentException If there is a variable with no value
*/
public static function resolve(string $template, array $vars, array $values): string
{
$map = [];
foreach ($vars as $var) {
if (false === strpos($template, '{'.$var.'}')) {
continue;
}
if (!isset($values[$var])) {
throw new \InvalidArgumentException(sprintf('The template "%s" contains the variable "%s", but was not given any value for it.', $template, $var));
}
$map['{'.$var.'}'] = $values[$var];
}
return strtr($template, $map);
}
public static function getCombinations(array $vars, array $values): array
{
if (!$vars) {
return [[]];
}
$combinations = [];
$nbValues = [];
foreach ($values as $var => $vals) {
if (!in_array($var, $vars, true)) {
continue;
}
$nbValues[$var] = count($vals);
}
for ($i = array_product($nbValues), $c = $i * 2; $i < $c; $i++) {
$k = $i;
$combination = [];
foreach ($vars as $var) {
$combination[$var] = $values[$var][$k % $nbValues[$var]];
$k = intval($k / $nbValues[$var]);
}
$combinations[] = $combination;
}
return $combinations;
}
final private function __construct()
{
}
}
================================================
FILE: src/Auth/AuthException.php
================================================
'Missing Attribute',
101 => 'Missing Login Attribute',
102 => 'Missing Password Attribute',
// Lookup errors
200 => 'User Not Found',
201 => 'Wrong Password',
// State errors
300 => 'User Not Activated',
301 => 'User Suspended',
302 => 'User Banned',
// Context errors
400 => 'User Not Logged In',
401 => 'User Forbidden',
];
/**
* __construct softens a detailed authentication error with a more vague message when
* the application is not in debug mode for security reasons.
* @param string $message
* @param int $code
* @param Exception $previous
*/
public function __construct($message = '', $code = 0, ?Exception $previous = null)
{
if ($this->useSoftErrors()) {
$message = static::$errorMessage;
}
if (isset(static::$errorCodes[$code])) {
$this->errorType = static::$errorCodes[$code];
}
parent::__construct(__($message), $code, $previous);
}
/**
* setDefaultErrorMessage will override the soft error message displayed to the user
*/
public static function setDefaultErrorMessage(string $message)
{
static::$errorMessage = $message;
}
/**
* useSoftErrors determines if soft errors should be used, set by config and when
* enabled uses less specific error messages.
*/
protected function useSoftErrors(): bool
{
if (Config::get('system.soft_auth_errors') !== null) {
return (bool) Config::get('system.soft_auth_errors');
}
return !Config::get('app.debug', false);
}
}
================================================
FILE: src/Auth/Concerns/HasGuard.php
================================================
checkCache !== null) {
return $this->checkCache;
}
if (is_null($this->user)) {
// Find persistence code
$userArray = $this->getPersistCodeFromSession();
if (!$userArray) {
return false;
}
[$id, $persistCode] = $userArray;
// Look up user
if (!$user = $this->findUserById($id)) {
return $this->checkCache = false;
}
// Confirm the persistence code is valid, otherwise reject
if (!$user->checkPersistCode($persistCode)) {
return $this->checkCache = false;
}
// Pass
$this->user = $user;
}
// Check cached user is activated
if (!($user = $this->getUser()) || ($this->requireActivation && !$user->is_activated)) {
return false;
}
// Throttle check
if ($this->useThrottle) {
$throttle = $this->findThrottleByUserId($user->getKey(), $this->ipAddress);
if ($throttle->is_banned || $throttle->checkSuspended()) {
$this->logout();
return false;
}
}
// Role impersonation
if ($this->isRoleImpersonator()) {
$this->applyRoleImpersonation($this->user);
}
return true;
}
/**
* guest determines if the current user is a guest.
* @return bool
*/
public function guest()
{
return false;
}
/**
* user will return the currently authenticated user.
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
return $this->getUser();
}
/**
* id for the currently authenticated user.
* @return int|null
*/
public function id()
{
if ($user = $this->getUser()) {
return $user->getAuthIdentifier();
}
return null;
}
/**
* validate a user's credentials.
* @return bool
*/
public function validate(array $credentials = [])
{
return !!$this->validateInternal($credentials);
}
/**
* hasUser determines if the guard has a user instance.
* @return bool
*/
public function hasUser()
{
return !is_null($this->user);
}
/**
* setUser will set the current user.
*/
public function setUser(Authenticatable $user)
{
$this->user = $user;
}
}
================================================
FILE: src/Auth/Concerns/HasImpersonation.php
================================================
getPersistCodeFromSession(false);
$oldUserId = $userArray ? $userArray[0] : null;
/**
* @event model.auth.beforeImpersonate
*
* Example usage:
*
* $model->bindEvent('model.auth.beforeImpersonate', function (\October\Rain\Database\Model|null $oldUser) use (\October\Rain\Database\Model $model) {
* traceLog($oldUser->full_name . ' is now impersonating ' . $model->full_name);
* });
*
*/
$oldUser = $oldUserId ? $this->findUserById($oldUserId) : null;
$user->fireEvent('model.auth.beforeImpersonate', [$oldUser]);
// Replace session with impersonated user
$this->setPersistCodeToSession($user, false, true);
// If this is the first time impersonating, capture the original user
if (!$this->isImpersonator()) {
Session::put($this->sessionKey.'_impersonate', $oldUserId ?: 'NaN');
}
}
/**
* stopImpersonate stops the current session being impersonated and
* attempts to authenticate as the impersonator again.
*/
public function stopImpersonate()
{
// Determine current and previous user
$userArray = $this->getPersistCodeFromSession(false);
$currentUserId = $userArray ? $userArray[0] : null;
$oldUser = $this->getImpersonator();
if ($currentUserId && ($currentUser = $this->findUserById($currentUserId))) {
/**
* @event model.auth.afterImpersonate
*
* Example usage:
*
* $model->bindEvent('model.auth.afterImpersonate', function (\October\Rain\Database\Model|null $oldUser) use (\October\Rain\Database\Model $model) {
* traceLog($oldUser->full_name . ' has stopped impersonating ' . $model->full_name);
* });
*
*/
$currentUser->fireEvent('model.auth.afterImpersonate', [$oldUser]);
}
// Restore previous user, if possible
if ($oldUser) {
$this->setPersistCodeToSession($oldUser, false, true);
}
else {
Session::forget($this->sessionKey);
}
Session::forget($this->sessionKey.'_impersonate');
}
/**
* getRealUser gets the "real" user to bypass impersonation.
* @return Authenticatable|null
*/
public function getRealUser()
{
if ($user = $this->getImpersonator()) {
return $user;
}
return $this->getUser();
}
/**
* isImpersonator checks to see if the current session is being impersonated.
* @return bool
*/
public function isImpersonator()
{
return Session::has($this->sessionKey.'_impersonate');
}
/**
* getImpersonator gets the original user doing the impersonation
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function getImpersonator()
{
if (!Session::has($this->sessionKey.'_impersonate')) {
return null;
}
$oldUserId = Session::get($this->sessionKey.'_impersonate');
if ((!is_string($oldUserId) && !is_int($oldUserId)) || $oldUserId === 'NaN') {
return null;
}
return $this->createUserModel()->find($oldUserId);
}
/**
* impersonateRole will impersonate a role for the current user
*/
public function impersonateRole($role)
{
Session::put($this->sessionKey.'_impersonate_role', $role->getKey());
}
/**
* isRoleImpersonator
*/
public function isRoleImpersonator(): bool
{
return !empty(Session::has($this->sessionKey.'_impersonate_role'));
}
/**
* stopImpersonateRole will stop role impersonation
*/
public function stopImpersonateRole()
{
Session::forget($this->sessionKey.'_impersonate_role');
}
/**
* applyRoleImpersonation tells the user model to impersonate the role
*/
protected function applyRoleImpersonation($user)
{
$roleId = Session::get($this->sessionKey.'_impersonate_role');
if ($role = $this->createRoleModel()->find($roleId)) {
$user->setRoleImpersonation($role);
}
}
}
================================================
FILE: src/Auth/Concerns/HasProviderProxy.php
================================================
userModel;
}
}
================================================
FILE: src/Auth/Concerns/HasSession.php
================================================
persist_code
? $user->persist_code
: $user->getPersistCode();
$toPersist = [$user->getKey(), $persistCode];
Session::put($this->sessionKey, $toPersist);
if ($remember) {
Cookie::queue(Cookie::forever($this->sessionKey, json_encode($toPersist)));
}
}
/**
* getPersistCodeFromSession will return the user ID and persist token from the session.
* The resulting array will contain the user ID and persistence code [id, code] or null.
*/
protected function getPersistCodeFromSession(bool $isChecking = true): ?array
{
// Check session first, followed by cookie
if ($sessionArray = Session::get($this->sessionKey)) {
$userArray = $sessionArray;
}
elseif ($cookieArray = Cookie::get($this->sessionKey)) {
if ($isChecking) {
$this->viaRemember = true;
}
$userArray = @json_decode($cookieArray, true);
}
else {
return null;
}
// Check supplied session/cookie is an array (user id, persist code)
if (!is_array($userArray) || count($userArray) !== 2) {
return null;
}
return $userArray;
}
}
================================================
FILE: src/Auth/Concerns/HasStatefulGuard.php
================================================
authenticate($credentials, $remember);
}
/**
* once logs a user into the application without sessions or cookies.
* @param array $credentials
* @return bool
*/
public function once(array $credentials = [])
{
$this->useSession = false;
$user = $this->authenticate($credentials);
$this->useSession = true;
return !!$user;
}
/**
* login the given user and sets properties in the session.
* @throws AuthException If the user is not activated and $this->requireActivation = true
*/
public function login(Authenticatable $user, $remember = true)
{
// Fire the 'beforeLogin' event
$user->beforeLogin();
// Activation is required, user not activated
if ($this->requireActivation && !$user->is_activated) {
throw new AuthException('Cannot login user since they are not activated.', 300);
}
$this->user = $user;
// Create session/cookie data to persist the session
if ($this->useSession) {
$this->setPersistCodeToSession($user, $remember);
}
// Fire the 'afterLogin' event
$user->afterLogin();
}
/**
* loginUsingId logs the given user ID into the application.
* @param mixed $id
* @param bool $remember
* @return \Illuminate\Contracts\Auth\Authenticatable|bool
*/
public function loginUsingId($id, $remember = false)
{
if (!is_null($user = $this->findUserById($id))) {
$this->login($user, $remember);
return $user;
}
return false;
}
/**
* onceUsingId logs the given user ID into the application without sessions or cookies.
* @param mixed $id
* @return \Illuminate\Contracts\Auth\Authenticatable|false
*/
public function onceUsingId($id)
{
if (!is_null($user = $this->findUserById($id))) {
$this->setUser($user);
return $user;
}
return false;
}
/**
* viaRemember determines if the user was authenticated via "remember me" cookie.
* @return bool
*/
public function viaRemember()
{
return $this->viaRemember;
}
/**
* logout logs the current user out.
*/
public function logout()
{
// Initialize the current auth session before trying to remove it
if (is_null($this->user) && !$this->check()) {
return;
}
if ($this->isImpersonator()) {
$this->user = $this->getImpersonator();
$this->stopImpersonate();
return;
}
if ($this->user) {
$this->user->setRememberToken(null);
$this->user->forceSave();
}
$this->user = null;
Session::invalidate();
Cookie::queue(Cookie::forget($this->sessionKey));
}
}
================================================
FILE: src/Auth/Concerns/HasThrottle.php
================================================
throttleModel, '\\');
return new $class();
}
/**
* findThrottleByLogin and ip address
*
* @param string $loginName
* @param string $ipAddress
* @return Models\Throttle
*/
public function findThrottleByLogin($loginName, $ipAddress)
{
$user = $this->findUserByLogin($loginName);
if (!$user) {
throw new AuthException('A user was not found with the given credentials.', 200);
}
$userId = $user->getKey();
return $this->findThrottleByUserId($userId, $ipAddress);
}
/**
* findThrottleByUserId and ip address
*
* @param integer $userId
* @param string $ipAddress
* @return Models\Throttle
*/
public function findThrottleByUserId($userId, $ipAddress = null)
{
$cacheKey = md5($userId.$ipAddress);
if (isset($this->throttle[$cacheKey])) {
return $this->throttle[$cacheKey];
}
$model = $this->createThrottleModel();
$query = $model->where('user_id', '=', $userId);
if ($ipAddress) {
$query->where(function ($query) use ($ipAddress) {
$query->where('ip_address', '=', $ipAddress);
$query->orWhere('ip_address', '=', null);
});
}
if (!$throttle = $query->first()) {
$throttle = $this->createThrottleModel();
$throttle->user_id = $userId;
if ($ipAddress) {
$throttle->ip_address = $ipAddress;
}
$throttle->save();
}
return $this->throttle[$cacheKey] = $throttle;
}
/**
* clearThrottleForUserId unsuspends and clears all throttles records for a user
*/
public function clearThrottleForUserId($userId)
{
if (!$userId) {
return;
}
$model = $this->createThrottleModel();
$throttles = $model->where('user_id', $userId)->get();
foreach ($throttles as $throttle) {
$throttle->unsuspend();
}
}
}
================================================
FILE: src/Auth/Concerns/HasUser.php
================================================
userModel, '\\');
return new $class();
}
/**
* createRoleModel creates an instance of the role model.
* @return Models\Role
*/
public function createRoleModel()
{
$class = '\\'.ltrim($this->roleModel, '\\');
return new $class();
}
/**
* createUserModelQuery prepares a query derived from the user model.
* @return \October\Rain\Database\Builder $query
*/
protected function createUserModelQuery()
{
$model = $this->createUserModel();
$query = $model->newQuery();
$this->extendUserQuery($query);
return $query;
}
/**
* extendUserQuery used for finding the user.
* @param \October\Rain\Database\Builder $query
*/
public function extendUserQuery($query)
{
}
/**
* hasSession returns true if a user session exists without verifying it.
*/
public function hasSession(): bool
{
return Session::has($this->sessionKey);
}
/**
* hasRemember returns true if the user requested to stay logged in.
*/
public function hasRemember(): bool
{
return Cookie::has($this->sessionKey);
}
/**
* getUser returns the current user, if any.
* @return Authenticatable|null
*/
public function getUser()
{
if (is_null($this->user)) {
$this->check();
}
return $this->user;
}
/**
* findUserById finds a user by the login value.
* @param string $id
* @return Authenticatable|null
*/
public function findUserById($id)
{
$query = $this->createUserModelQuery();
$user = $query->find($id);
return $this->validateUserModel($user) ? $user : null;
}
/**
* findUserByLogin finds a user by the login value.
* @param string $login
* @return Authenticatable|null
*/
public function findUserByLogin($login)
{
$model = $this->createUserModel();
$query = $this->createUserModelQuery();
$user = $query->where($model->getLoginName(), $login)->first();
return $this->validateUserModel($user) ? $user : null;
}
/**
* findUserByCredentials finds a user by the given credentials.
* @param array $credentials
* @throws AuthException
* @return Models\User
*/
public function findUserByCredentials(array $credentials)
{
$model = $this->createUserModel();
$loginName = $model->getLoginName();
if (!array_key_exists($loginName, $credentials)) {
throw new AuthException("The {$loginName} attribute is required.", 101);
}
$query = $this->createUserModelQuery();
$hashableAttributes = $model->getHashableAttributes();
$hashedCredentials = [];
/*
* Build query from given credentials
*/
foreach ($credentials as $credential => $value) {
// All excepted the hashed attributes
if (in_array($credential, $hashableAttributes)) {
$hashedCredentials = array_merge($hashedCredentials, [$credential => $value]);
}
else {
$query = $query->where($credential, '=', $value);
}
}
$user = $query->first();
if (!$this->validateUserModel($user)) {
throw new AuthException('A user was not found with the given credentials.', 200);
}
/*
* Check the hashed credentials match
*/
foreach ($hashedCredentials as $credential => $value) {
if (!$user->checkHashValue($credential, $value)) {
// Incorrect password
if ($credential === 'password') {
throw new AuthException('A user was found but the password did not match.', 201);
}
// User not found
throw new AuthException('A user was not found with the given credentials.', 200);
}
}
return $user;
}
/**
* validateUserModel perform additional checks on the user model.
* @param object $user
* @return bool
*/
protected function validateUserModel($user)
{
return $user instanceof $this->userModel;
}
}
================================================
FILE: src/Auth/Manager.php
================================================
$this->throttleModel]
*/
protected $throttle = [];
/**
* @var string userModel class
*/
protected $userModel = Models\User::class;
/**
* @var string roleModel class
*/
protected $roleModel = Models\Role::class;
/**
* @var string groupModel class
*/
protected $groupModel = Models\Group::class;
/**
* @var string throttleModel class
*/
protected $throttleModel = Models\Throttle::class;
/**
* @var bool useThrottle flag to enable login throttling.
*/
protected $useThrottle = true;
/**
* @var bool useRehash flag to enable password rehashing.
*/
protected $useRehash = true;
/**
* @var bool useSession internal flag to toggle using the session for
* the current authentication request.
*/
protected $useSession = true;
/**
* @var bool requireActivation rlag to require users to be activated to login.
*/
protected $requireActivation = true;
/**
* @var string sessionKey to store the auth session data in.
*/
protected $sessionKey = 'october_auth';
/**
* @var bool viaRemember indicates if the user was authenticated via a recaller cookie.
*/
protected $viaRemember = false;
/**
* @var string ipAddress of this request.
*/
public $ipAddress = '0.0.0.0';
/**
* @var bool|null checkCache adds a specific cache to the check() method to reduce
* the number of database calls.
*/
protected $checkCache = null;
/**
* init the singleton
*/
protected function init()
{
$this->ipAddress = Request::ip();
}
/**
* register a user with the provided credentials with optional flags for
* activating the newly created user and automatically logging them in.
*
* @param array $credentials
* @param bool $activate
* @param bool $autoLogin
* @return Models\User
*/
public function register(array $credentials, $activate = false, $autoLogin = true)
{
$user = $this->createUserModel();
$user->fill($credentials);
$user->save();
if ($activate) {
$user->attemptActivation($user->getActivationCode());
}
// Prevents revalidation of the password field
// on subsequent saves to this model object
$user->password = null;
if ($autoLogin) {
$this->user = $user;
}
return $user;
}
/**
* authenticate the given user according to the passed credentials
*/
public function authenticate(array $credentials, $remember = true)
{
$user = $this->validateInternal($credentials);
$user->clearResetPassword();
$this->login($user, $remember);
return $this->user;
}
/**
* validateInternal a user's credentials, method used internally.
* @return Models\User
*/
protected function validateInternal(array $credentials = [])
{
// Default to the login name field or fallback to a hard-coded 'login' value
$loginName = $this->createUserModel()->getLoginName();
$loginCredentialKey = isset($credentials[$loginName]) ? $loginName : 'login';
if (empty($credentials[$loginCredentialKey])) {
throw new AuthException("The {$loginCredentialKey} attribute is required.", 100);
}
if (empty($credentials['password'])) {
throw new AuthException('The password attribute is required.', 102);
}
// If the fallback 'login' was provided and did not match the necessary
// login name, swap it over
if ($loginCredentialKey !== $loginName) {
$credentials[$loginName] = $credentials[$loginCredentialKey];
unset($credentials[$loginCredentialKey]);
}
// If throttling is enabled, check they are not locked out first and foremost.
if ($this->useThrottle) {
$throttle = $this->findThrottleByLogin($credentials[$loginName], $this->ipAddress);
$throttle->check();
}
// Look up the user by authentication credentials.
try {
$user = $this->findUserByCredentials($credentials);
}
catch (AuthException $ex) {
if ($this->useThrottle) {
$throttle->addLoginAttempt();
}
throw $ex;
}
if ($this->useThrottle) {
$throttle->clearLoginAttempts();
}
// Rehash password if needed
if ($this->useRehash) {
$user->attemptRehashPassword($credentials['password']);
}
return $user;
}
}
================================================
FILE: src/Auth/Migrations/2013_10_01_000001_Db_Users.php
================================================
increments('id');
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('login')->unique()->index();
$table->string('email')->unique();
$table->string('password');
$table->string('activation_code')->nullable()->index();
$table->string('persist_code')->nullable();
$table->string('reset_password_code')->nullable()->index();
$table->text('permissions')->nullable();
$table->boolean('is_activated')->default(0);
$table->boolean('is_superuser')->default(false);
$table->timestamp('activated_at')->nullable();
$table->timestamp('last_login')->nullable();
$table->integer('role_id')->unsigned()->nullable()->index();
$table->timestamps();
});
}
public function down()
{
Schema::drop('users');
}
};
================================================
FILE: src/Auth/Migrations/2013_10_01_000002_Db_Groups.php
================================================
increments('id');
$table->string('name')->unique();
$table->string('code')->nullable()->index();
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::drop('groups');
}
};
================================================
FILE: src/Auth/Migrations/2013_10_01_000003_Db_Users_Groups.php
================================================
integer('user_id')->unsigned();
$table->integer('group_id')->unsigned();
$table->primary(['user_id', 'group_id']);
});
}
public function down()
{
Schema::drop('users_groups');
}
};
================================================
FILE: src/Auth/Migrations/2013_10_01_000004_Db_Preferences.php
================================================
increments('id');
$table->integer('user_id')->unsigned();
$table->string('namespace', 100);
$table->string('group', 50);
$table->string('item', 150);
$table->text('value')->nullable();
$table->index(['user_id', 'namespace', 'group', 'item'], 'user_item_index');
});
}
public function down()
{
Schema::drop('preferences');
}
};
================================================
FILE: src/Auth/Migrations/2013_10_01_000005_Db_Throttle.php
================================================
increments('id');
$table->integer('user_id')->unsigned()->nullable()->index();
$table->string('ip_address')->nullable()->index();
$table->integer('attempts')->default(0);
$table->timestamp('last_attempt_at')->nullable();
$table->boolean('is_suspended')->default(0);
$table->timestamp('suspended_at')->nullable();
$table->boolean('is_banned')->default(0);
$table->timestamp('banned_at')->nullable();
});
}
public function down()
{
Schema::drop('throttle');
}
};
================================================
FILE: src/Auth/Migrations/2017_10_01_000006_Db_Roles.php
================================================
increments('id');
$table->string('name')->unique();
$table->text('permissions')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::drop('roles');
}
};
================================================
FILE: src/Auth/Models/Group.php
================================================
'required|between:4,16|unique:groups',
];
/**
* @var array belongsToMany relationship
*/
public $belongsToMany = [
'users' => [User::class, 'table' => 'users_groups']
];
/**
* @var array fillable fields
*/
protected $fillable = [
'name',
'code',
'description',
];
/**
* delete the group
* @return bool
*/
public function delete()
{
$this->users()->detach();
return parent::delete();
}
}
================================================
FILE: src/Auth/Models/Preferences.php
================================================
getUser();
if (!$user) {
throw new AuthException('User is not logged in', 400);
}
return $user;
}
/**
* forUser creates this object and sets the user context
*/
public static function forUser($user = null)
{
$self = new static;
$self->userContext = $user ?: $self->resolveUser($user);
return $self;
}
/**
* get returns a setting value by the module (or plugin) name and setting name
* @param string $key Specifies the setting key value, for example 'backend:items.perpage'
* @param mixed $default The default value to return if the setting doesn't exist in the DB.
* @return mixed Returns the setting value loaded from the database or the default value.
*/
public function get($key, $default = null)
{
if (!($user = $this->userContext)) {
return $default;
}
$cacheKey = $this->getCacheKey($key, $user);
if (array_key_exists($cacheKey, static::$cache)) {
return static::$cache[$cacheKey];
}
$record = static::findRecord($key, $user);
if (!$record) {
return static::$cache[$cacheKey] = $default;
}
return static::$cache[$cacheKey] = $record->value;
}
/**
* set stores a setting value to the database
* @param string $key Specifies the setting key value, for example 'backend:items.perpage'
* @param mixed $value The setting value to store, serializable.
* If the user is not provided the currently authenticated user will be used. If there is no
* an authenticated user, the exception will be thrown.
* @return bool
*/
public function set($key, $value)
{
if (!$user = $this->userContext) {
return false;
}
$record = static::findRecord($key, $user);
if (!$record) {
list($namespace, $group, $item) = $this->parseKey($key);
$record = new static;
$record->namespace = $namespace;
$record->group = $group;
$record->item = $item;
$record->user_id = $user->id;
}
$record->value = $value;
$record->save();
$cacheKey = $this->getCacheKey($key, $user);
static::$cache[$cacheKey] = $value;
return true;
}
/**
* reset a setting value by deleting the record
* @param string $key Specifies the setting key value.
* @return bool
*/
public function reset($key)
{
if (!$user = $this->userContext) {
return false;
}
$record = static::findRecord($key, $user);
if (!$record) {
return false;
}
$record->delete();
$cacheKey = $this->getCacheKey($key, $user);
unset(static::$cache[$cacheKey]);
return true;
}
/**
* findRecord returns a record for a user
* @return self
*/
public static function findRecord($key, $user = null)
{
return static::applyKeyAndUser($key, $user)->first();
}
/**
* scopeApplyKeyAndUser to find a setting record for the specified module (or plugin) name,
* setting name and user.
*
* @param string $key Specifies the setting key value, for example 'backend:items.perpage'
* @param mixed $default The default value to return if the setting doesn't exist in the DB.
* @param mixed $user An optional user object.
* @return mixed Returns the found record or null.
*/
public function scopeApplyKeyAndUser($query, $key, $user = null)
{
list($namespace, $group, $item) = $this->parseKey($key);
$query = $query
->where('namespace', $namespace)
->where('group', $group)
->where('item', $item);
if ($user) {
$query = $query->where('user_id', $user->id);
}
return $query;
}
/**
* getCacheKey builds a cache key for the preferences record
* @return string
*/
protected function getCacheKey($item, $user)
{
return $user->id . '-' . $item;
}
}
================================================
FILE: src/Auth/Models/Role.php
================================================
'required|between:4,16|unique:role',
];
/**
* @var array hasMany relationship
*/
public $hasMany = [
'users' => User::class
];
/**
* @var array jsonable attribute names that are json encoded and decoded from the database
*/
protected $jsonable = ['permissions'];
/**
* @var array allowedPermissionsValues
*
* Possible options:
* 0 => Remove.
* 1 => Add.
*/
protected $allowedPermissionsValues = [0, 1];
/**
* @var array fillable fields
*/
protected $fillable = [
'name',
];
/**
* hasAccess will see if a role has access to the passed permission(s)
*
* If multiple permissions are passed, the role must
* have access to all permissions passed through, unless the
* "all" flag is set to false.
*
* @param string|array $permissions
* @param bool $all
* @return bool
*/
public function hasAccess($permissions, $all = true)
{
$rolePermissions = $this->permissions;
if (!is_array($permissions)) {
$permissions = (array) $permissions;
}
foreach ($permissions as $permission) {
// We will set a flag now for whether this permission was
// matched at all.
$matched = true;
// Now, let's check if the permission ends in a wildcard "*" symbol.
// If it does, we'll check through all the merged permissions to see
// if a permission exists which matches the wildcard.
if ((strlen($permission) > 1) && str_ends_with($permission, '*')) {
$matched = false;
foreach ($rolePermissions as $rolePermission => $value) {
// Strip the '*' off the end of the permission.
$checkPermission = substr($permission, 0, -1);
// We will make sure that the merged permission does not
// exactly match our permission, but starts with it.
if (
$checkPermission !== $rolePermission &&
str_starts_with($rolePermission, $checkPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
}
// Now, let's check if the permission starts in a wildcard "*" symbol.
// If it does, we'll check through all the merged permissions to see
// if a permission exists which matches the wildcard.
elseif ((strlen($permission) > 1) && str_starts_with($permission, '*')) {
$matched = false;
foreach ($rolePermissions as $rolePermission => $value) {
// Strip the '*' off the start of the permission.
$checkPermission = substr($permission, 1);
// We will make sure that the merged permission does not
// exactly match our permission, but ends with it.
if (
$checkPermission !== $rolePermission &&
str_ends_with($rolePermission, $checkPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
}
else {
$matched = false;
foreach ($rolePermissions as $rolePermission => $value) {
// This time check if the rolePermission ends in wildcard "*" symbol.
if ((strlen($rolePermission) > 1) && str_ends_with($rolePermission, '*')) {
$matched = false;
// Strip the '*' off the end of the permission.
$checkGroupPermission = substr($rolePermission, 0, -1);
// We will make sure that the merged permission does not
// exactly match our permission, but starts with it.
if (
$checkGroupPermission !== $permission &&
str_starts_with($permission, $checkGroupPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
// Otherwise, we'll fallback to standard permissions checking where
// we match that permissions explicitly exist.
elseif (
$permission === $rolePermission &&
(int) $rolePermissions[$permission] === 1
) {
$matched = true;
break;
}
}
}
// Now, we will check if we have to match all
// permissions or any permission and return
// accordingly.
if ($all === true && $matched === false) {
return false;
}
elseif ($all === false && $matched === true) {
return true;
}
}
return !($all === false);
}
/**
* hasAnyAccess returns if the user has access to any of the given permissions
* @param array $permissions
* @return bool
*/
public function hasAnyAccess(array $permissions)
{
return $this->hasAccess($permissions, false);
}
/**
* setPermissionsAttribute validates the permissions when set
* @param string $permissions
* @return void
*/
public function setPermissionsAttribute($permissions)
{
$permissions = json_decode($permissions, true);
foreach ($permissions as $permission => $value) {
if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) {
throw new InvalidArgumentException(sprintf(
'Invalid value "%s" for permission "%s" given.',
$value,
$permission
));
}
if ($value === 0) {
unset($permissions[$permission]);
}
}
$this->attributes['permissions'] = !empty($permissions) ? json_encode($permissions) : '';
}
}
================================================
FILE: src/Auth/Models/Throttle.php
================================================
[User::class, 'key' => 'user_id']
];
/**
* @var bool timestamps indicates if the model should be timestamped
*/
public $timestamps = false;
/**
* @var array dates attributes that should be mutated to dates
*/
protected $dates = ['last_attempt_at', 'suspended_at', 'banned_at'];
/**
* @var int attemptLimit
*/
protected static $attemptLimit = 5;
/**
* @var int suspensionTime in minutes
*/
protected static $suspensionTime = 15;
/**
* getUser returns the associated user with the throttler
* @return User
*/
public function getUser()
{
return $this->user()->getResults();
}
/**
* getLoginAttempts
* @return int
*/
public function getLoginAttempts()
{
if ($this->attempts > 0 && $this->last_attempt_at) {
$this->clearLoginAttemptsIfAllowed();
}
return (int) $this->attempts;
}
/**
* addLoginAttempt
*/
public function addLoginAttempt()
{
$this->attempts++;
$this->last_attempt_at = $this->freshTimestamp();
if ($this->getLoginAttempts() >= static::$attemptLimit) {
$this->suspend();
}
else {
$this->save();
}
}
/**
* clearLoginAttempts
*/
public function clearLoginAttempts()
{
// If our login attempts is already at zero we do not need to do anything. Additionally,
// if we are suspended, we are not going to do anything either as clearing login attempts
// makes us unsuspended. We need to manually call unsuspend() in order to unsuspend.
if ($this->getLoginAttempts() === 0 || $this->is_suspended) {
return;
}
$this->attempts = 0;
$this->last_attempt_at = null;
$this->is_suspended = false;
$this->suspended_at = null;
$this->save();
}
/**
* suspend the user associated with the throttle
*/
public function suspend()
{
if (!$this->is_suspended) {
$this->is_suspended = true;
$this->suspended_at = $this->freshTimestamp();
$this->save();
}
}
/**
* unsuspend the user
*/
public function unsuspend()
{
if ($this->is_suspended) {
$this->attempts = 0;
$this->last_attempt_at = null;
$this->is_suspended = false;
$this->suspended_at = null;
$this->save();
}
}
/**
* checkSuspended checks if the user is suspended
* @return bool
*/
public function checkSuspended()
{
if ($this->is_suspended && $this->suspended_at) {
$this->removeSuspensionIfAllowed();
return (bool) $this->is_suspended;
}
return false;
}
/**
* ban the user
* @return void
*/
public function ban()
{
if (!$this->is_banned) {
$this->is_banned = true;
$this->banned_at = $this->freshTimestamp();
$this->save();
}
}
/**
* unban the user
* @return void
*/
public function unban()
{
if ($this->is_banned) {
$this->is_banned = false;
$this->banned_at = null;
$this->save();
}
}
/**
* check user throttle status
* @return bool
* @throws AuthException
*/
public function check()
{
if ($this->is_banned) {
throw new AuthException('Cannot login user since they are banned.', 302);
}
if ($this->checkSuspended()) {
throw new AuthException('Cannot login user since they are suspended.', 301);
}
return true;
}
/**
* clearLoginAttemptsIfAllowed inspects the last attempt vs the suspension time
* (the time in which attempts must space before the account is suspended).
* If we can clear our attempts now, we'll do so and save.
*
* @return void
*/
public function clearLoginAttemptsIfAllowed()
{
$lastAttempt = clone $this->last_attempt_at;
$suspensionTime = static::$suspensionTime;
$clearAttemptsAt = $lastAttempt->modify("+{$suspensionTime} minutes");
$now = new Carbon;
if ($clearAttemptsAt <= $now) {
$this->attempts = 0;
$this->save();
}
unset($lastAttempt, $clearAttemptsAt, $now);
}
/**
* removeSuspensionIfAllowed inspects to see if the user can become unsuspended
* or not, based on the suspension time provided. If so, unsuspends.
*/
public function removeSuspensionIfAllowed()
{
$suspended = clone $this->suspended_at;
$suspensionTime = static::$suspensionTime;
$unsuspendAt = $suspended->modify("+{$suspensionTime} minutes");
$now = new Carbon;
if ($unsuspendAt <= $now) {
$this->unsuspend();
}
unset($suspended, $unsuspendAt, $now);
}
/**
* getIsSuspendedAttribute is a get mutator for the suspended property
* @param mixed $suspended
* @return bool
*/
public function getIsSuspendedAttribute($suspended)
{
return (bool) $suspended;
}
/**
* getIsBannedAttribute is a get mutator for the banned property
* @param mixed $banned
* @return bool
*/
public function getIsBannedAttribute($banned)
{
return (bool) $banned;
}
}
================================================
FILE: src/Auth/Models/User.php
================================================
'required|between:3,255|email|unique:users',
'password' => 'required:create|min:2|confirmed',
'password_confirmation' => 'required_with:password|min:2'
];
/**
* @var array belongsToMany relation
*/
public $belongsToMany = [
'groups' => [Group::class, 'table' => 'users_groups']
];
/**
* @var array belongsTo relation
*/
public $belongsTo = [
'role' => Role::class
];
/**
* @var array dates attributes that should be mutated to dates
*/
protected $dates = ['activated_at', 'last_login'];
/**
* @var array hidden attributes removed from the API representation of the model (ex. toArray())
*/
protected $hidden = ['password', 'reset_password_code', 'activation_code', 'persist_code'];
/**
* @var array fillable fields
*/
protected $fillable = [
'first_name',
'last_name',
'login',
'email',
'password',
'password_confirmation',
];
/**
* @var array hashable list of attribute names which should be hashed using the Bcrypt hashing algorithm
*/
protected $hashable = ['password', 'persist_code'];
/**
* @var array purgeable list of attribute names which should not be saved to the database
*/
protected $purgeable = ['password_confirmation'];
/**
* @var array attributeNames array of custom attribute names
*/
public $attributeNames = [];
/**
* @var array customMessages array of custom error messages
*/
public $customMessages = [];
/**
* @var array jsonable attribute names that are json encoded and decoded from the database
*/
protected $jsonable = ['permissions'];
/**
* allowedPermissionsValues
*
* Possible options:
* -1 => Deny (adds to array, but denies regardless of user's group).
* 0 => Remove.
* 1 => Add.
*
* @var array
*/
protected $allowedPermissionsValues = [-1, 0, 1];
/**
* @var string loginAttribute
*/
public static $loginAttribute = 'email';
/**
* @var string rememberTokenName is the column name of the "remember me" token
*/
protected $rememberTokenName = 'persist_code';
/**
* @var array mergedPermissions for the user
*/
protected $mergedPermissions;
/**
* @var Role impersonatingRole set if the user is impersonating a role
*/
protected $impersonatingRole;
/**
* @return string getLoginName returns the name for the user's login
*/
public function getLoginName()
{
return static::$loginAttribute;
}
/**
* @return mixed getLogin returns the user's login
*/
public function getLogin()
{
return $this->{$this->getLoginName()};
}
/**
* isSuperUser checks if the user is a super user - has access to everything
* regardless of permissions
* @return bool
*/
public function isSuperUser()
{
if ($this->impersonatingRole) {
return false;
}
return (bool) $this->is_superuser;
}
//
// Events
//
/**
* beforeLogin event
*/
public function beforeLogin()
{
}
/**
* afterLogin event
*/
public function afterLogin()
{
$this->last_login = $this->freshTimestamp();
$this->forceSave();
}
/**
* afterDelete deletes the user groups
* @return bool
*/
public function afterDelete()
{
if ($this->hasRelation('groups')) {
$this->groups()->detach();
}
}
//
// Persistence (used by Cookies and Sessions)
//
/**
* getPersistCode gets a code for when the user is persisted to a cookie or session
* which identifies the user
* @return string
*/
public function getPersistCode()
{
$this->persist_code = $this->getRandomString();
// Our code got hashed
$persistCode = $this->persist_code;
$this->forceSave();
return $persistCode;
}
/**
* checkPersistCode checks the given persist code
* @param string $persistCode
* @return bool
*/
public function checkPersistCode($persistCode)
{
if (!$persistCode || !$this->persist_code) {
return false;
}
return $persistCode === $this->persist_code;
}
//
// Activation
//
/**
* getIsActivatedAttribute is a get mutator for giving the activated property
* @param mixed $activated
* @return bool
*/
public function getIsActivatedAttribute($activated)
{
return (bool) $activated;
}
/**
* getActivationCode for the given user
* @return string
*/
public function getActivationCode()
{
$this->activation_code = $activationCode = $this->getRandomString();
$this->forceSave();
return $activationCode;
}
/**
* attemptActivation the given user by checking the activate code. If the user
* is activated already, an Exception is thrown
* @param string $activationCode
* @return bool
*/
public function attemptActivation($activationCode)
{
if ($this->is_activated) {
throw new Exception('User is already active!');
}
if ($activationCode === $this->activation_code) {
$this->activation_code = null;
$this->is_activated = true;
$this->activated_at = $this->freshTimestamp();
$this->forceSave();
return true;
}
return false;
}
//
// Password
//
/**
* checkPassword checks the password passed matches the user's password
* @param string $password
* @return bool
*/
public function checkPassword($password)
{
return Hash::check($password, $this->password);
}
/**
* getResetPasswordCode gets a reset password code for the given user
* @return string
*/
public function getResetPasswordCode()
{
$this->reset_password_code = $resetCode = $this->getRandomString();
$this->forceSave();
return $resetCode;
}
/**
* checkResetPasswordCode checks if the provided user reset password code is
* valid without actually resetting the password
* @param string $resetCode
* @return bool
*/
public function checkResetPasswordCode($resetCode)
{
if (!$resetCode || !$this->reset_password_code) {
return false;
}
return $this->reset_password_code === $resetCode;
}
/**
* attemptResetPassword a user's password by matching the reset code generated with the users
* @param string $resetCode
* @param string $newPassword
* @return bool
*/
public function attemptResetPassword($resetCode, $newPassword)
{
if ($this->checkResetPasswordCode($resetCode)) {
$this->password = $newPassword;
$this->reset_password_code = null;
if ($this->is_password_expired) {
$this->is_password_expired = false;
}
return $this->forceSave();
}
return false;
}
/**
* attemptRehashPassword will check if a password needs to be rehashed and apply the
* new hashing algorithm to the current password supplied as plaintext.
*/
public function attemptRehashPassword(string $currentPassword): bool
{
if (!Hash::needsRehash($this->password)) {
return false;
}
if (!$this->checkPassword($currentPassword)) {
throw new Exception('Cannot rehash using a new password!');
}
// Rehash via the Hashable trait
$this->password = $currentPassword;
return $this->forceSave();
}
/**
* clearResetPassword wipes out the data associated with resetting a password
* @return void
*/
public function clearResetPassword()
{
if ($this->reset_password_code) {
$this->reset_password_code = null;
$this->forceSave();
}
}
/**
* setPasswordAttribute protects the password from being reset to null
*/
public function setPasswordAttribute($value)
{
if ($this->exists && empty($value)) {
unset($this->attributes['password']);
}
else {
$this->attributes['password'] = $value;
// Password has changed, log out all users
$this->attributes['persist_code'] = null;
}
}
//
// Permissions, Groups & Role
//
/**
* getGroups returns an array of groups which the given user belongs to
* @return array
*/
public function getGroups()
{
return $this->groups;
}
/**
* getRole returns the role assigned to this user
* @return October\Rain\Auth\Models\Role
*/
public function getRole()
{
return $this->role;
}
/**
* setRoleImpersonation set to the role to impersonate or null to disable.
*/
public function setRoleImpersonation(?Role $role): void
{
$this->impersonatingRole = $role;
}
/**
* getRoleImpersonation
*/
public function getRoleImpersonation(): ?Role
{
return $this->impersonatingRole;
}
/**
* addGroup adds the user to the given group
* @param Group $group
* @return bool
*/
public function addGroup($group)
{
if (!$this->inGroup($group)) {
$this->groups()->attach($group);
$this->unsetRelation('groups');
}
return true;
}
/**
* removeGroup removes the user from the given group
* @param Group $group
* @return bool
*/
public function removeGroup($group)
{
if ($this->inGroup($group)) {
$this->groups()->detach($group);
$this->unsetRelation('groups');
}
return true;
}
/**
* inGroup see if the user is in the given group
* @param Group $group
* @return bool
*/
public function inGroup($group)
{
foreach ($this->getGroups() as $_group) {
if ($_group->getKey() === $group->getKey()) {
return true;
}
}
return false;
}
/**
* getMergedPermissions returns an array of merged permissions for each group the user is in
* @return array
*/
public function getMergedPermissions()
{
if (!$this->mergedPermissions) {
$permissions = [];
if (($role = $this->getRole()) && is_array($role->permissions)) {
$permissions = array_merge($permissions, $role->permissions);
}
if (is_array($this->permissions)) {
$permissions = array_merge($permissions, $this->permissions);
}
$this->mergedPermissions = $permissions;
}
return $this->mergedPermissions;
}
/**
* hasAccess sees if a user has access to the passed permission(s). Permissions are merged
* from all groups the user belongs to and then are checked against the passed permission(s).
*
* If multiple permissions are passed, the user must have access to all permissions passed
* through, unless the "all" flag is set to false.
*
* Super users have access no matter what.
*
* @param string|array $permissions
* @param bool $all
* @return bool
*/
public function hasAccess($permissions, $all = true)
{
if ($this->isSuperUser()) {
return true;
}
return $this->hasPermission($permissions, $all);
}
/**
* hasPermission sees if a user has access to the passed permission(s). Permissions are merged
* from all groups the user belongs to and then are checked against the passed permission(s).
*
* If multiple permissions are passed, the user must have access to all permissions passed
* through, unless the "all" flag is set to false.
*
* Super users don't have access no matter what.
*
* @param string|array $permissions
* @param bool $all
* @return bool
*/
public function hasPermission($permissions, $all = true)
{
if ($this->impersonatingRole) {
$mergedPermissions = (array) $this->impersonatingRole->permissions;
}
else {
$mergedPermissions = $this->getMergedPermissions();
}
if (!is_array($permissions)) {
$permissions = [$permissions];
}
foreach ($permissions as $permission) {
// We will set a flag now for whether this permission was
// matched at all.
$matched = true;
// Now, let's check if the permission ends in a wildcard "*" symbol.
// If it does, we'll check through all the merged permissions to see
// if a permission exists which matches the wildcard.
if ((strlen($permission) > 1) && str_ends_with($permission, '*')) {
$matched = false;
foreach ($mergedPermissions as $mergedPermission => $value) {
// Strip the '*' off the end of the permission.
$checkPermission = substr($permission, 0, -1);
// We will make sure that the merged permission does not
// exactly match our permission, but starts with it.
if (
$checkPermission !== $mergedPermission &&
str_starts_with($mergedPermission, $checkPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
}
elseif ((strlen($permission) > 1) && str_starts_with($permission, '*')) {
$matched = false;
foreach ($mergedPermissions as $mergedPermission => $value) {
// Strip the '*' off the beginning of the permission.
$checkPermission = substr($permission, 1);
// We will make sure that the merged permission does not
// exactly match our permission, but ends with it.
if (
$checkPermission !== $mergedPermission &&
str_ends_with($mergedPermission, $checkPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
}
else {
$matched = false;
foreach ($mergedPermissions as $mergedPermission => $value) {
// This time check if the mergedPermission ends in wildcard "*" symbol.
if ((strlen($mergedPermission) > 1) && str_ends_with($mergedPermission, '*')) {
$matched = false;
// Strip the '*' off the end of the permission.
$checkMergedPermission = substr($mergedPermission, 0, -1);
// We will make sure that the merged permission does not
// exactly match our permission, but starts with it.
if (
$checkMergedPermission !== $permission &&
str_starts_with($permission, $checkMergedPermission) &&
(int) $value === 1
) {
$matched = true;
break;
}
}
// Otherwise, we'll fallback to standard permissions checking where
// we match that permissions explicitly exist.
elseif (
$permission === $mergedPermission &&
(int) $mergedPermissions[$permission] === 1
) {
$matched = true;
break;
}
}
}
// Now, we will check if we have to match all permissions or any permission and return
// accordingly.
if ($all === true && $matched === false) {
return false;
}
elseif ($all === false && $matched === true) {
return true;
}
}
return !($all === false);
}
/**
* hasAnyAccess returns if the user has access to any of the given permissions
* @param array $permissions
* @return bool
*/
public function hasAnyAccess(array $permissions)
{
return $this->hasAccess($permissions, false);
}
/**
* setPermissionsAttribute validates any set permissions
* @param string $permissions
* @return void
*/
public function setPermissionsAttribute($permissions)
{
$permissions = json_decode($permissions, true) ?: [];
foreach ($permissions as $permission => &$value) {
if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) {
throw new InvalidArgumentException(sprintf(
'Invalid value "%s" for permission "%s" given.',
$value,
$permission
));
}
if ($value === 0) {
unset($permissions[$permission]);
}
}
$this->attributes['permissions'] = !empty($permissions) ? json_encode($permissions) : '';
}
//
// User Interface
//
/**
* getAuthIdentifierName gets the name of the unique identifier for the user
* @return string
*/
public function getAuthIdentifierName()
{
return $this->getKeyName();
}
/**
* getAuthPasswordName of the password attribute for the user.
*/
public function getAuthPasswordName()
{
return 'password';
}
/**
* getAuthIdentifier gets the unique identifier for the user
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->{$this->getAuthIdentifierName()};
}
/**
* getAuthPassword gets the password for the user
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* getReminderEmail gets the e-mail address where password reminders are sent
* @return string
*/
public function getReminderEmail()
{
return $this->email;
}
/**
* getRememberToken gets the token value for the "remember me" session
* @return string
*/
public function getRememberToken()
{
return $this->getPersistCode();
}
/**
* setRememberToken sets the token value for the "remember me" session
* @param string $value
* @return void
*/
public function setRememberToken($value)
{
$this->persist_code = $value;
}
/**
* getRememberTokenName gets the column name for the "remember me" token
* @return string
*/
public function getRememberTokenName()
{
return $this->rememberTokenName;
}
//
// Helpers
//
/**
* getRandomString generates a random string
* @return string
*/
public function getRandomString($length = 42)
{
return Str::random($length);
}
}
================================================
FILE: src/Composer/ClassLoader.php
================================================
basePath = $basePath;
}
/**
* instance returns the class loader instance
*/
public static function instance(): ?static
{
return static::$loader;
}
/**
* configure the loader
*/
public static function configure($basePath)
{
return static::$loader = new static($basePath);
}
/**
* withNamespace
*/
public function withNamespace($namespace, $directory): static
{
$this->namespaces[$namespace] = $directory;
return $this;
}
/**
* withDirectories to the class loader
* @param string|array $directories
*/
public function withDirectories($directories): static
{
$this->directories = array_merge($this->directories, (array) $directories);
$this->directories = array_unique($this->directories);
return $this;
}
/**
* load the given class file
* @param string $class
*/
public function load($class): bool
{
if (!str_contains($class, '\\')) {
return false;
}
if (
isset($this->manifest[$class]) &&
is_file($fullPath = $this->basePath.DIRECTORY_SEPARATOR.$this->manifest[$class])
) {
require $fullPath;
return true;
}
if (isset($this->unknownClasses[$class])) {
return false;
}
// Load namespaces
foreach ($this->namespaces as $namespace => $directory) {
if (substr($class, 0, strlen($namespace)) === $namespace) {
$classWithoutNamespace = substr($class, strlen($namespace));
[$lowerClass, $upperClass] = $this->normalizeClass($classWithoutNamespace);
if ($this->loadUpperOrLower($class, $directory, $upperClass, $lowerClass) === true) {
return true;
}
}
}
// Load directories
[$lowerClass, $upperClass] = $this->normalizeClass($class);
foreach ($this->directories as $directory) {
if ($this->loadUpperOrLower($class, $directory, $upperClass, $lowerClass) === true) {
return true;
}
}
$this->unknownClasses[$class] = true;
return false;
}
/**
* register the given class loader on the auto-loader stack
*/
public function register()
{
if ($this->registered) {
return;
}
$this->registered = spl_autoload_register(function($class) {
$this->load($class);
});
}
/**
* build the manifest and write it to disk
*/
public function build()
{
if (!$this->manifestDirty) {
return;
}
$this->write($this->manifest);
}
/**
* initManifest starts the manifest cache file after registration.
*/
public function initManifest(string $manifestPath)
{
$this->manifestPath = $manifestPath;
$this->ensureManifestIsLoaded();
}
/**
* removeDirectories from the class loader
* @param string|array $directories
*/
public function removeDirectories($directories = null)
{
if (is_null($directories)) {
$this->directories = [];
}
else {
$directories = (array) $directories;
$this->directories = array_filter($this->directories, function ($directory) use ($directories) {
return !in_array($directory, $directories);
});
}
}
/**
* getDirectories registered with the loader
*/
public function getDirectories(): array
{
return $this->directories;
}
/**
* loadUpperOrLower loads a class in a directory with the supplied upper and lower class path.
*/
protected function loadUpperOrLower(string $class, string $directory, string $upperClass, string $lowerClass): bool
{
if ($directory) {
$directory .= DIRECTORY_SEPARATOR;
}
if ($this->isRealFilePath($path = $directory.$lowerClass)) {
$this->includeClass($class, $path);
return true;
}
if ($this->isRealFilePath($path = $directory.$upperClass)) {
$this->includeClass($class, $path);
return true;
}
return false;
}
/**
* isRealFilePath determines if a relative path to a file exists and is real
*/
protected function isRealFilePath(string $path): bool
{
return is_file(realpath($this->basePath.DIRECTORY_SEPARATOR.$path));
}
/**
* includeClass and add to the manifest
*/
protected function includeClass(string $class, string $path)
{
require $this->basePath.DIRECTORY_SEPARATOR.$path;
// Normalize path
$this->manifest[$class] = str_replace('\\', '/', $path);
$this->manifestDirty = true;
}
/**
* normalizeClass get the normal file name for a class
*/
protected function normalizeClass(string $class): array
{
// Strip first slash
if ($class[0] === '\\') {
$class = substr($class, 1);
}
// Lowercase folders
$parts = explode('\\', $class);
$file = array_pop($parts);
$namespace = implode('\\', $parts);
$directory = str_replace(['\\', '_'], DIRECTORY_SEPARATOR, $namespace);
// Provide both alternatives
$lowerClass = strtolower($directory) . DIRECTORY_SEPARATOR . $file . '.php';
$upperClass = $directory . DIRECTORY_SEPARATOR . $file . '.php';
return [$lowerClass, $upperClass];
}
/**
* ensureManifestIsLoaded has been loaded into memory
*/
protected function ensureManifestIsLoaded()
{
$manifest = [];
if (file_exists($this->manifestPath)) {
try {
$manifest = require $this->manifestPath;
if (!is_array($manifest)) {
$manifest = [];
}
}
catch (Throwable $ex) {}
}
$this->manifest += $manifest;
}
/**
* write the given manifest array to disk
*/
protected function write(array $manifest)
{
if ($this->manifestPath === null) {
return;
}
if (!is_writable(dirname($this->manifestPath))) {
throw new Exception("The directory [{$this->manifestPath}] must be present and writable.");
}
file_put_contents(
$this->manifestPath,
'setOutput();
}
/**
* instance creates a new instance of this singleton
*/
public static function instance(): static
{
try {
return App::make('core.composer');
}
catch (Exception $ex) {
return new static;
}
}
/**
* update runs the "composer update" command
*/
public function update(array $packages = [])
{
$this->assertEnvironmentReady();
$this->assertHomeVariableSet();
try {
$this->assertHomeDirectory();
$this->assertComposerWarmedUp();
Installer::create($this->output, $this->makeComposer())
->setDevMode(Config::get('app.debug', false))
->setUpdateAllowList($packages)
->setPreferDist()
->setUpdate(true)
->run();
}
finally {
$this->assertWorkingDirectory();
}
}
/**
* require runs the "composer require" command
*/
public function require(array $requirements)
{
$this->assertEnvironmentReady();
$this->assertHomeVariableSet();
$this->backupComposerFile();
$statusCode = 1;
$lastException = new Exception('Failed to update composer dependencies');
try {
$this->assertHomeDirectory();
$this->assertComposerWarmedUp();
$this->writePackages($requirements);
$composer = $this->makeComposer();
$installer = Installer::create($this->output, $composer)
->setDevMode(Config::get('app.debug', false))
->setPreferDist()
->setUpdate(true)
->setUpdateAllowTransitiveDependencies(Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS);
// If no lock is present, or the file is brand new, we do not do a
// partial update as this is not supported by the Installer
if ($composer->getLocker()->isLocked()) {
$installer->setUpdateAllowList(array_keys($requirements));
}
$statusCode = $installer->run();
}
catch (Throwable $ex) {
$statusCode = 1;
$lastException = $ex;
}
finally {
$this->assertWorkingDirectory();
}
if ($statusCode !== 0) {
$this->restoreComposerFile();
throw $lastException;
}
}
/**
* remove runs the "composer remove" command
*/
public function remove(array $packageNames)
{
$requirements = [];
foreach ($packageNames as $package) {
$requirements[$package] = false;
}
$this->require($requirements);
}
/**
* addPackages without update
*/
public function addPackages(array $requirements)
{
$this->writePackages($requirements);
}
/**
* removePackages without update
*/
public function removePackages(array $packageNames)
{
$requirements = [];
foreach ($packageNames as $package) {
$requirements[$package] = false;
}
$this->writePackages($requirements);
}
/**
* getPackageVersions returns version numbers for the specified packages
*/
public function getPackageVersions(array $packageNames): array
{
$result = [];
$packages = $this->listAllPackages();
foreach ($packageNames as $wantPackage) {
$wantPackageLower = mb_strtolower($wantPackage);
foreach ($packages as $package) {
if (!isset($package['name'])) {
continue;
}
if (mb_strtolower($package['name']) === $wantPackageLower) {
$result[$wantPackage] = $package['version'] ?? null;
}
}
}
return $result;
}
/**
* hasPackage returns true if the specified package is installed
*/
public function hasPackage($name): bool
{
$name = mb_strtolower($name);
return array_key_exists($name, $this->getPackageVersions([$name]));
}
/**
* listPackages returns a list of directly installed packages
*/
public function listPackages()
{
return $this->listPackagesInternal();
}
/**
* listAllPackages returns a list of installed packages, including dependencies
*/
public function listAllPackages()
{
return $this->listPackagesInternal(false);
}
/**
* addRepository will add a repository to the composer config
*/
public function addRepository($name, $type, $address, $options = [])
{
$file = new JsonFile($this->getJsonPath());
$config = new JsonConfigSource($file);
$config->addRepository($name, array_merge([
'type' => $type,
'url' => $address
], $options));
}
/**
* removeRepository will remove a repository from the composer config
*/
public function removeRepository($name)
{
$file = new JsonFile($this->getJsonPath());
$config = new JsonConfigSource($file);
$config->removeConfigSetting($name);
}
/**
* hasRepository return true if the composer config contains the repo address
*/
public function hasRepository($address): bool
{
$file = new JsonFile($this->getJsonPath());
$config = $file->read();
$repos = $config['repositories'] ?? [];
foreach ($repos as $repo) {
if (!isset($repo['url'])) {
continue;
}
if (rtrim($repo['url'], '/') === $address) {
return true;
}
}
return false;
}
/**
* addAuthCredentials will add credentials to an auth config file
*/
public function addAuthCredentials($hostname, $username, $password, $type = null)
{
if ($type === null) {
$type = 'http-basic';
}
$file = new JsonFile($this->getAuthPath());
$config = new JsonConfigSource($file, true);
$config->addConfigSetting($type.'.'.$hostname, [
'username' => $username,
'password' => $password
]);
}
/**
* getAuthCredentials returns auth credentials added to the config file
*/
public function getAuthCredentials($hostname, $type = null): ?array
{
if ($type === null) {
$type = 'http-basic';
}
$authFile = $this->getAuthPath();
$config = json_decode(file_get_contents($authFile), true);
return $config[$type][$hostname] ?? null;
}
/**
* makeComposer returns a new instance of composer
*/
protected function makeComposer(): Composer
{
$composer = Factory::create($this->output);
// Disable scripts
$composer->getEventDispatcher()->setRunScripts(false);
// Discard changes to prevent corrupt state
$composer->getConfig()->merge([
'config' => [
'discard-changes' => true
]
]);
return $composer;
}
/**
* listPackagesInternal returns a list of installed packages
*/
protected function listPackagesInternal($useDirect = true)
{
$composerLock = base_path('vendor/composer/installed.json');
$composerFile = $this->getJsonPath();
$installedPackages = json_decode(file_get_contents($composerLock), true);
$packages = $installedPackages['packages'] ?? [];
$filter = [];
if ($useDirect) {
$composerPackages = json_decode(file_get_contents($composerFile), true);
$require = array_merge(
$composerPackages['require'] ?? [],
$composerPackages['require-dev'] ?? []
);
foreach ($require as $pkg => $ver) {
$filter[$pkg] = true;
}
}
$result = [];
foreach ($packages as $package) {
$name = $package['name'] ?? '';
if ($useDirect && !isset($filter[$name])) {
continue;
}
$result[] = [
'name' => $name,
'version' => $this->normalizeVersion($package['version'] ?? ''),
'description' => $package['description'] ?? '',
];
}
return $result;
}
/**
* normalizeVersion
*/
protected function normalizeVersion($packageVersion)
{
$version = (new VersionParser)->normalize($packageVersion);
$parts = explode('.', $version);
if (count($parts) === 4 && preg_match('{^0\D?}', $parts[3])) {
unset($parts[3]);
$version = implode('.', $parts);
}
return $version;
}
/**
* getJsonPath returns a path to the composer.json file
*/
protected function getJsonPath(): string
{
return base_path('composer.json');
}
/**
* getAuthPath returns a path to the auth.json file
*/
protected function getAuthPath(): string
{
return base_path('auth.json');
}
}
================================================
FILE: src/Composer/Concerns/HasAssertions.php
================================================
workingDir = getcwd();
chdir(dirname($this->getJsonPath()));
}
/**
* assertWorkingDirectory
*/
protected function assertWorkingDirectory()
{
chdir($this->workingDir);
}
/**
* assertComposerWarmedUp preloads composer in case it wants to update itself
*/
protected function assertComposerWarmedUp()
{
// Preload root package
$this->assertPackageLoaded('Composer', base_path('vendor/composer/composer/src/Composer'), false);
// Preload child packages
$preload = [
'Composer\Autoload',
'Composer\Config',
'Composer\DependencyResolver',
'Composer\Downloader',
'Composer\EventDispatcher',
'Composer\Exception',
'Composer\Filter',
'Composer\Installer',
'Composer\IO',
'Composer\Json',
'Composer\Package',
'Composer\Platform',
'Composer\Plugin',
'Composer\Question',
'Composer\Repository',
'Composer\Script',
'Composer\SelfUpdate',
'Composer\Util',
];
foreach ($preload as $package) {
$this->assertPackageLoaded(
$package,
base_path('vendor/composer/composer/src/'.str_replace("\\", "/", $package))
);
}
}
/**
* assertPackageLoaded ensures all classes in a package are loaded
*/
protected function assertPackageLoaded($packageName, $packagePath, $recursive = true)
{
$allFiles = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($packagePath))
: new DirectoryIterator($packagePath);
$phpFiles = new RegexIterator($allFiles, '/\.php$/');
$packagePathLen = strlen($packagePath);
foreach ($phpFiles as $phpFile) {
// Remove base directory and .php extension
$className = substr($phpFile->getRealPath(), $packagePathLen, -4);
// Normalize OS path separators, normalize to a class namespace
$className = trim(str_replace("/", "\\", $className), '\\');
// Build complete namespace
$className = $packageName . '\\' . $className;
// Preload class
class_exists($className);
}
}
}
================================================
FILE: src/Composer/Concerns/HasAutoloader.php
================================================
loader === null && file_exists($autoLoadFile = base_path('vendor/autoload.php'))) {
$this->loader = include $autoLoadFile;
$this->preloadPools();
}
}
/**
* autoload is a similar function to including vendor/autoload.php.
* @param string $vendorPath Absoulte path to the vendor directory.
* @return void
*/
public function autoload($vendorPath)
{
$this->initAutoloader();
$dir = $vendorPath . '/composer';
if (file_exists($file = $dir . '/autoload_namespaces.php')) {
$map = require $file;
foreach ($map as $namespace => $path) {
if (isset($this->namespacePool[$namespace])) {
continue;
}
$this->loader->set($namespace, $path);
$this->namespacePool[$namespace] = true;
}
}
if (file_exists($file = $dir . '/autoload_psr4.php')) {
$map = require $file;
foreach ($map as $namespace => $path) {
if (isset($this->psr4Pool[$namespace])) {
continue;
}
$this->loader->setPsr4($namespace, $path);
$this->psr4Pool[$namespace] = true;
}
}
if (file_exists($file = $dir . '/autoload_classmap.php')) {
$classMap = require $file;
if ($classMap) {
$classMapDiff = array_diff_key($classMap, $this->classMapPool);
$this->loader->addClassMap($classMapDiff);
$this->classMapPool += array_fill_keys(array_keys($classMapDiff), true);
}
}
if (file_exists($file = $dir . '/autoload_files.php')) {
$includeFiles = require $file;
foreach ($includeFiles as $includeFile) {
$relativeFile = $this->stripVendorDir($includeFile, $vendorPath);
if (isset($this->includeFilesPool[$relativeFile])) {
continue;
}
require $includeFile;
$this->includeFilesPool[$relativeFile] = true;
}
}
}
/**
* preloadPools
*/
protected function preloadPools()
{
$this->classMapPool = array_fill_keys(array_keys($this->loader->getClassMap()), true);
$this->namespacePool = array_fill_keys(array_keys($this->loader->getPrefixes()), true);
$this->psr4Pool = array_fill_keys(array_keys($this->loader->getPrefixesPsr4()), true);
$this->includeFilesPool = $this->preloadIncludeFilesPool();
}
/**
* preloadIncludeFilesPool
*/
protected function preloadIncludeFilesPool()
{
$result = [];
$vendorPath = base_path() .'/vendor';
if (file_exists($file = $vendorPath . '/composer/autoload_files.php')) {
$includeFiles = require $file;
foreach ($includeFiles as $includeFile) {
$relativeFile = $this->stripVendorDir($includeFile, $vendorPath);
$result[$relativeFile] = true;
}
}
return $result;
}
/**
* stripVendorDir removes the vendor directory from a path.
* @param string $path
* @return string
*/
protected function stripVendorDir($path, $vendorDir)
{
$path = realpath($path);
$vendorDir = realpath($vendorDir);
if (strpos($path, $vendorDir) === 0) {
$path = substr($path, strlen($vendorDir));
}
return $path;
}
}
================================================
FILE: src/Composer/Concerns/HasOctoberCommands.php
================================================
addRepository(
'octobercms',
'composer',
$url,
[
'only' => ['october/*', '*-plugin', '*-theme']
]
);
}
}
================================================
FILE: src/Composer/Concerns/HasOutput.php
================================================
output = new NullIO();
}
else {
$this->output = $output;
}
}
/**
* setOutputCommand
*/
public function setOutputCommand(Command $command, InputInterface $input)
{
$this->setOutput(new ConsoleIO($input, $command->getOutput(), $command->getHelperSet()));
}
/**
* setOutputBuffer
*/
public function setOutputBuffer()
{
$this->setOutput(new BufferIO());
}
/**
* getOutputBuffer
*/
public function getOutputBuffer(): string
{
if ($this->output instanceof BufferIO) {
return $this->output->getOutput();
}
return '';
}
}
================================================
FILE: src/Composer/Concerns/HasRequirements.php
================================================
getJsonPath());
$result = null;
// Update cleanly
$contents = file_get_contents($json->getPath());
$manipulator = new JsonManipulator($contents);
foreach ($requirements as $package => $version) {
if ($version !== false) {
$result = $manipulator->addLink($requireKey, $package, $version, $sortPackages);
}
else {
$result = $manipulator->removeSubNode($requireKey, $package);
}
if ($result) {
$result = $manipulator->removeSubNode($removeKey, $package);
}
}
if ($result) {
$manipulator->removeMainKeyIfEmpty($removeKey);
file_put_contents($json->getPath(), $manipulator->getContents());
return;
}
// Fallback update
$composerDefinition = $json->read();
foreach ($requirements as $package => $version) {
if ($version !== false) {
$composerDefinition[$requireKey][$package] = $version;
}
else {
unset($composerDefinition[$requireKey][$package]);
}
unset($composerDefinition[$removeKey][$package]);
if (isset($composerDefinition[$removeKey]) && count($composerDefinition[$removeKey]) === 0) {
unset($composerDefinition[$removeKey]);
}
}
$json->write($composerDefinition);
}
/**
* restoreComposerFile
*/
protected function restoreComposerFile()
{
if ($this->composerBackup) {
file_put_contents($this->getJsonPath(), $this->composerBackup);
}
}
/**
* backupComposerFile
*/
protected function backupComposerFile()
{
$this->composerBackup = file_get_contents($this->getJsonPath());
}
}
================================================
FILE: src/Composer/resources/file_get_contents.php
================================================
files()->name('*.php')->in($configPath) as $file) {
$directory = self::getNestedDirectory($file, $configPath);
$files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();
}
ksort($files, SORT_NATURAL);
return $files;
}
/**
* Get the configuration file nesting path.
*
* @param \SplFileInfo $file
* @param string $configPath
* @return string
*/
protected static function getNestedDirectory(SplFileInfo $file, $configPath)
{
$directory = $file->getPath();
if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR)) {
$nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.';
}
return $nested;
}
}
================================================
FILE: src/Config/README.md
================================================
Config
=======
An extension of illuminate\config
Modules and plugins can have config files in the /config directory. Plugin and module configuration files are registered automatically.
## Accessing configuration strings
````
// Get a configuration string from the CMS module
echo Config::get('cms::options.allow_comments');
// Get a configuration string from the october/blog plugin.
echo Config::get('october.blog::options.allow_comments');
````
## Overriding configuration strings
System users can override configuration strings without altering the modules' and plugins' files. This is done by adding configuration files to the app/config directory. To override a plugin's configuration:
````
app
config
vendorname
pluginname
file.php
````
Example: config/october/blog/options.php
To override a module's configuration:
````
app
config
modulename
file.php
````
Example: config/cms/options.php
================================================
FILE: src/Config/Repository.php
================================================
$filePath) {
// Filenames with config.php are treated as root nodes
$configKey = $key === 'config' ? $namespace : $namespace . '.' . $key;
// Core config overrides package config
$coreConfig = $this->get($configKey, []);
$baseConfig = require $filePath;
$this->set($configKey, $coreConfig + $baseConfig);
}
}
/**
* has determines if the given configuration value exists.
*
* @param string $key
* @return bool
*/
public function has($key)
{
return Arr::has($this->items, $this->toNsKey($key));
}
/**
* get the specified configuration value.
*
* @param array|string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
if (is_array($key)) {
return $this->getMany($key);
}
return Arr::get($this->items, $this->toNsKey($key), $default);
}
/**
* getMany configuration values.
*
* @param array $keys
* @return array
*/
public function getMany($keys)
{
$config = [];
foreach ($keys as $key => $default) {
if (is_numeric($key)) {
[$key, $default] = [$default, null];
}
$newKey = $this->toNsKey($key);
$config[$newKey] = Arr::get($this->items, $newKey, $default);
}
return $config;
}
/**
* set a given configuration value.
*
* @param array|string $key
* @param mixed $value
* @return void
*/
public function set($key, $value = null)
{
$keys = is_array($key) ? $key : [$key => $value];
foreach ($keys as $key => $value) {
Arr::set($this->items, $this->toNsKey($key), $value);
}
}
/**
* toNsKey converts a namespaced key to an array key
*/
protected function toNsKey($key)
{
if (strpos($key, '::') !== false) {
return str_replace(['::config', '::'], ['', '.'], $key);
}
return $key;
}
}
================================================
FILE: src/Database/Attach/File.php
================================================
[]
];
/**
* @var array fillable attributes are mass assignable
*/
protected $fillable = [
'file_name',
'title',
'description',
'field',
'attachment_id',
'attachment_type',
'is_public',
'sort_order',
'data',
];
/**
* @var array guarded attributes aren't mass assignable
*/
protected $guarded = [];
/**
* @var array imageExtensions known
*/
public static $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'];
/**
* @var array hidden fields from array/json access
*/
protected $hidden = ['attachment_type', 'attachment_id', 'is_public'];
/**
* @var array appends fields to array/json access
*/
protected $appends = ['path', 'extension'];
/**
* @var mixed data is a local file name or an instance of an uploaded file,
* objects of the UploadedFile class.
*/
public $data = null;
/**
* @var array autoMimeTypes
*/
protected $autoMimeTypes = [
'docx' => 'application/msword',
'xlsx' => 'application/excel',
'gif' => 'image/gif',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'avif' => 'image/avif',
'pdf' => 'application/pdf',
'svg' => 'image/svg+xml',
];
//
// Constructors
//
/**
* fromPost creates a file object from a file an uploaded file, the input can be an
* upload object or the input name from a file postback.
* @param string|UploadedFile $fileInput
* @return $this
*/
public function fromPost($fileInput)
{
if (is_string($fileInput)) {
$fileInput = files($fileInput);
}
if (!$fileInput) {
return;
}
$this->file_name = $fileInput->getClientOriginalName();
$this->file_size = $fileInput->getSize();
$this->content_type = $fileInput->getMimeType();
$this->disk_name = $this->getDiskName();
// getRealPath() can be empty for some environments (IIS)
$realPath = empty(trim($fileInput->getRealPath()))
? $fileInput->getPath() . DIRECTORY_SEPARATOR . $fileInput->getFileName()
: $fileInput->getRealPath();
$this->putFile($realPath, $this->disk_name);
return $this;
}
/**
* fromFile creates a file object from a file on the disk
* @param string $filePath
* @param string $filename
* @return $this
*/
public function fromFile($filePath, $filename = null)
{
if ($filePath === null) {
return;
}
$file = new FileObj($filePath);
$this->file_name = empty($filename) ? $file->getFilename() : $filename;
$this->file_size = $file->getSize();
$this->content_type = $file->getMimeType();
$this->disk_name = $this->getDiskName();
$this->putFile($file->getRealPath(), $this->disk_name);
return $this;
}
/**
* fromData creates a file object from raw data
* @param string $data
* @param string $filename
*/
public function fromData($data, $filename)
{
if ($data === null) {
return;
}
$tempName = str_replace('.', '', uniqid('', true)) . '.tmp';
$tempPath = temp_path($tempName);
FileHelper::put($tempPath, $data);
$file = $this->fromFile($tempPath, basename($filename));
FileHelper::delete($tempPath);
return $file;
}
/**
* fromUrl creates a file object from url
* @param string $url
* @param string $filename
* @return self
*/
public function fromUrl($url, $filename = null)
{
$data = Http::get($url);
if ($data->status() !== 200) {
throw new Exception(sprintf('Error getting file "%s", error code: %d', $url, $data->status()));
}
if (empty($filename)) {
$filename = basename(parse_url($url, PHP_URL_PATH));
}
return $this->fromData($data->body(), $filename);
}
//
// Attribute mutators
//
/**
* getUrlAttribute helper attribute for getUrl
* @return string
*/
public function getUrlAttribute()
{
return $this->getUrl();
}
/**
* @deprecated see getUrlAttribute
*/
public function getPathAttribute()
{
return $this->getPath();
}
/**
* getExtensionAttribute helper attribute for getExtension
* @return string
*/
public function getExtensionAttribute()
{
return $this->getExtension();
}
/**
* setDataAttribute used only when filling attributes
*/
public function setDataAttribute($value)
{
$this->data = $value;
}
/**
* getWidthAttribute helper attribute for get image width
* @return string|null
*/
public function getWidthAttribute()
{
if (!$this->isImage()) {
return null;
}
$dimensions = $this->getImageDimensions();
if (!$dimensions) {
return null;
}
return $dimensions[0];
}
/**
* getHeightAttribute helper attribute for get image height
* @return string|null
*/
public function getHeightAttribute()
{
if (!$this->isImage()) {
return null;
}
$dimensions = $this->getImageDimensions();
if (!$dimensions) {
return null;
}
return $dimensions[1];
}
/**
* getSizeAttribute helper attribute for file size in human format
* @return string
*/
public function getSizeAttribute()
{
return $this->sizeToString();
}
//
// Output and Download
//
/**
* download the file contents
* @return Response
*/
public function download()
{
return Response::download($this->getLocalPath(), $this->file_name);
}
/**
* output the raw file contents
* @param string $disposition see the download method @deprecated
* @param bool $returnResponse Direct output will be removed soon, chain with ->send() @deprecated
* @return Response
*/
public function output($disposition = 'inline', $returnResponse = true)
{
if ($disposition === 'attachment') {
return $this->download();
}
$response = Response::file($this->getLocalPath());
if ($returnResponse) {
return $response;
}
$response->send();
}
/**
* outputThumb the raw thumb file contents
* @param integer $width
* @param integer $height
* @param array $options [
* 'mode' => 'auto',
* 'offset' => [0, 0],
* 'quality' => 90,
* 'sharpen' => 0,
* 'interlace' => false,
* 'extension' => 'auto',
* 'disposition' => 'inline',
* ]
* @param bool $returnResponse Direct output will be removed soon, chain with ->send() @deprecated
* @todo Refactor thumb to resources and recommend it be local, if remote, still use content grabber
* @return Response|void
*/
public function outputThumb($width, $height, $options = [], $returnResponse = true)
{
$disposition = Arr::get($options, 'disposition', 'inline');
$options = $this->getDefaultThumbOptions($options);
// Generate thumb if not existing already
$thumbFile = $this->getThumbFilename($width, $height, $options);
if (
!$this->hasFile($thumbFile) &&
!$this->getThumb($width, $height, $options)
) {
throw new Exception(sprintf('Thumb file "%s" failed to generate. Check error logs for more details.', $thumbFile));
}
$contents = $this->getContents($thumbFile);
$response = Response::make($contents)->withHeaders([
'Content-type' => $this->getContentType(),
'Content-Disposition' => $disposition . '; filename="' . basename($thumbFile) . '"',
'Cache-Control' => 'private, no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
'Accept-Ranges' => 'bytes',
'Content-Length' => mb_strlen($contents, '8bit'),
]);
if ($returnResponse) {
return $response;
}
$response->send();
}
//
// Getters
//
/**
* getCacheKey returns the cache key used for the hasFile method
* @param string $path The path to get the cache key for
* @return string
*/
public function getCacheKey($path = null)
{
if (empty($path)) {
$path = $this->getDiskPath();
}
return 'database-file::' . $path;
}
/**
* getFilename returns the file name without path
*/
public function getFilename()
{
return $this->file_name;
}
/**
* getExtension returns the file extension
*/
public function getExtension()
{
return FileHelper::extension($this->file_name);
}
/**
* getLastModified returns the last modification date as a UNIX timestamp
* @return int
*/
public function getLastModified($fileName = null)
{
return $this->storageCmd('lastModified', $this->getDiskPath($fileName));
}
/**
* getContentType returns the file content type
*/
public function getContentType()
{
if ($this->content_type !== null) {
return $this->content_type;
}
$ext = $this->getExtension();
if (isset($this->autoMimeTypes[$ext])) {
return $this->content_type = $this->autoMimeTypes[$ext];
}
return null;
}
/**
* getContents from storage device
*/
public function getContents($fileName = null)
{
return $this->storageCmd('get', $this->getDiskPath($fileName));
}
/**
* getUrl returns a URL for this attachment
*/
public function getUrl()
{
return $this->getPath();
}
/**
* getPath returns the URL path to access this file or a thumb file
*/
public function getPath($fileName = null)
{
if (empty($fileName)) {
$fileName = $this->disk_name;
}
return $this->getPublicPath() . $this->getPartitionDirectory() . $fileName;
}
/**
* getLocalPath returns a local path to this file. If the file is stored remotely,
* it will be downloaded to a temporary directory.
*/
public function getLocalPath()
{
if ($this->isLocalStorage()) {
return $this->getLocalRootPath() . '/' . $this->getDiskPath();
}
$itemSignature = md5($this->getPath()) . $this->getLastModified();
$cachePath = $this->getLocalTempPath($itemSignature . '.' . $this->getExtension());
if (!FileHelper::exists($cachePath)) {
$this->copyStorageToLocal($this->getDiskPath(), $cachePath);
}
return $cachePath;
}
/**
* getDiskPath returns the path to the file, relative to the storage disk
* @return string
*/
public function getDiskPath($fileName = null)
{
if (empty($fileName)) {
$fileName = $this->disk_name;
}
return $this->getStorageDirectory() . $this->getPartitionDirectory() . $fileName;
}
/**
* isPublic determines if the file is flagged "public" or not
*/
public function isPublic()
{
if (array_key_exists('is_public', $this->attributes)) {
return $this->attributes['is_public'];
}
if (isset($this->is_public)) {
return $this->is_public;
}
return true;
}
/**
* sizeToString returns the file size as string
* @return string Returns the size as string.
*/
public function sizeToString()
{
return FileHelper::sizeToString($this->file_size);
}
//
// Events
//
/**
* beforeSave check if new file data has been supplied
* eg: $model->data = files('something');
*/
public function beforeSave()
{
// Process the data property
if ($this->data !== null) {
if ($this->data instanceof UploadedFile) {
$this->fromPost($this->data);
}
$this->data = null;
}
}
/**
* afterDelete clean up it's thumbnails
*/
public function afterDelete()
{
try {
if ($this->shouldDeleteFile()) {
$this->deleteThumbs();
$this->deleteFile();
}
}
catch (Exception $ex) {
}
}
//
// Image Handling
//
/**
* isImage checks if the file extension is an image and returns true or false
*/
public function isImage()
{
return in_array(strtolower($this->getExtension()), static::$imageExtensions);
}
/**
* getImageDimensions
* @return array|bool
*/
protected function getImageDimensions()
{
return getimagesize($this->getLocalPath());
}
/**
* getThumbUrl generates and returns a thumbnail URL path
*
* @param integer $width
* @param integer $height
* @param array $options [
* 'mode' => 'auto',
* 'offset' => [0, 0],
* 'quality' => 90,
* 'sharpen' => 0,
* 'interlace' => false,
* 'extension' => 'auto',
* ]
* @return string
*/
public function getThumbUrl($width, $height, $options = [])
{
if (!$this->isImage() || !$this->hasFile($this->disk_name)) {
return $this->getUrl();
}
$width = (int) $width;
$height = (int) $height;
$options = $this->getDefaultThumbOptions($options);
$thumbFile = $this->getThumbFilename($width, $height, $options);
$thumbPath = $this->getDiskPath($thumbFile);
$thumbPublic = $this->getPath($thumbFile);
if (!$this->hasFile($thumbFile)) {
try {
if ($this->isLocalStorage()) {
$this->makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options);
}
else {
$this->makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options);
}
}
catch (Exception $ex) {
Log::error($ex);
return '';
}
}
return $thumbPublic;
}
/**
* getThumb is shorter syntax for getThumbUrl
* @return string
*/
public function getThumb($width, $height, $options = [])
{
return $this->getThumbUrl($width, $height, $options);
}
/**
* getThumbFilename generates a thumbnail filename
* @return string
*/
public function getThumbFilename($width, $height, $options)
{
$options = $this->getDefaultThumbOptions($options);
$mode = $options['mode'];
$extension = $options['extension'];
if (is_array($options['offset'] ?? null)) {
$offsetX = $options['offset'][0] ?? ($options['offset']['x'] ?? 0);
$offsetY = $options['offset'][1] ?? ($options['offset']['y'] ?? 0);
return "thumb_{$this->id}_{$width}_{$height}_{$offsetX}_{$offsetY}_{$mode}.{$extension}";
}
else {
return "thumb_{$this->id}_{$width}_{$height}_{$mode}.{$extension}";
}
}
/**
* getDefaultThumbOptions returns the default thumbnail options
* @return array
*/
protected function getDefaultThumbOptions($overrideOptions = [])
{
$defaultOptions = [
'mode' => 'auto',
'offset' => null,
'quality' => 90,
'sharpen' => 0,
'interlace' => false,
'extension' => 'auto',
];
if (!is_array($overrideOptions)) {
$overrideOptions = ['mode' => $overrideOptions];
}
$options = array_merge($defaultOptions, $overrideOptions);
$options['mode'] = strtolower($options['mode']);
if (strtolower($options['extension']) === 'auto') {
$options['extension'] = strtolower($this->getExtension());
}
return $options;
}
/**
* makeThumbLocal generates the thumbnail based on the local file system. This step
* is necessary to simplify things and ensure the correct file permissions are given
* to the local files.
*/
protected function makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options)
{
$rootPath = $this->getLocalRootPath();
$filePath = $rootPath.'/'.$this->getDiskPath();
$thumbPath = $rootPath.'/'.$thumbPath;
// Generate thumbnail
Resizer::open($filePath)
->resize($width, $height, $options)
->save($thumbPath)
;
FileHelper::chmod($thumbPath);
}
/**
* makeThumbStorage generates the thumbnail based on a remote storage engine
*/
protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options)
{
$tempFile = $this->getLocalTempPath();
$tempThumb = $this->getLocalTempPath($thumbFile);
// Generate thumbnail
$this->copyStorageToLocal($this->getDiskPath(), $tempFile);
try {
Resizer::open($tempFile)
->resize($width, $height, $options)
->save($tempThumb)
;
}
finally {
FileHelper::delete($tempFile);
}
// Publish to storage
$success = $this->copyLocalToStorage($tempThumb, $thumbPath);
// Clean up
FileHelper::delete($tempThumb);
// Eagerly cache remote exists call
if ($success) {
Cache::forever($this->getCacheKey($thumbPath), true);
}
}
/**
* deleteThumbs deletes all thumbnails for this file
*/
public function deleteThumbs()
{
$pattern = 'thumb_'.$this->id.'_';
$directory = $this->getStorageDirectory() . $this->getPartitionDirectory();
$allFiles = $this->storageCmd('files', $directory);
$collection = [];
foreach ($allFiles as $file) {
if (str_starts_with(basename($file), $pattern)) {
$collection[] = $file;
}
}
// Delete the collection of files
if (!empty($collection)) {
if ($this->isLocalStorage()) {
FileHelper::delete($collection);
}
else {
$this->getDisk()->delete($collection);
foreach ($collection as $filePath) {
Cache::forget($this->getCacheKey($filePath));
}
}
}
}
//
// File handling
//
/**
* getDiskName generates a disk name from the supplied file name
*/
protected function getDiskName()
{
if ($this->disk_name !== null) {
return $this->disk_name;
}
$ext = strtolower($this->getExtension());
$name = str_replace('.', '', uniqid('', true));
return $this->disk_name = !empty($ext) ? $name.'.'.$ext : $name;
}
/**
* getLocalTempPath returns a temporary local path to work from
*/
protected function getLocalTempPath($path = null)
{
if (!$path) {
return $this->getTempPath() . '/' . md5($this->getDiskPath()) . '.' . $this->getExtension();
}
return $this->getTempPath() . '/' . $path;
}
/**
* putFile saves a file
* @param string $sourcePath An absolute local path to a file name to read from.
* @param string $destinationFileName A storage file name to save to.
*/
protected function putFile($sourcePath, $destinationFileName = null)
{
if (!$destinationFileName) {
$destinationFileName = $this->disk_name;
}
$destinationPath = $this->getStorageDirectory() . $this->getPartitionDirectory();
if (!$this->isLocalStorage()) {
return $this->copyLocalToStorage($sourcePath, $destinationPath . $destinationFileName);
}
// Using local storage, tack on the root path and work locally
// this will ensure the correct permissions are used.
$destinationPath = $this->getLocalRootPath() . '/' . $destinationPath;
// Verify the directory exists, if not try to create it. If creation fails
// because the directory was created by a concurrent process then proceed,
// otherwise trigger the error.
if (
!FileHelper::isDirectory($destinationPath) &&
!FileHelper::makeDirectory($destinationPath, 0755, true, true) &&
!FileHelper::isDirectory($destinationPath)
) {
if (($lastErr = error_get_last()) !== null) {
trigger_error($lastErr['message'], E_USER_WARNING);
}
}
return FileHelper::copy($sourcePath, $destinationPath . $destinationFileName);
}
/**
* shouldDeleteFile returns true if the file should be deleted.
*/
protected function shouldDeleteFile($fileName = null): bool
{
if (!$fileName) {
$fileName = $this->disk_name;
}
if (!$fileName) {
return false;
}
return $this
->newQueryWithoutScopes()
->where('disk_name', $fileName)
->count() === 0;
}
/**
* deleteFile contents from storage device
*/
protected function deleteFile($fileName = null)
{
if (!$fileName) {
$fileName = $this->disk_name;
}
$directory = $this->getStorageDirectory() . $this->getPartitionDirectory();
$filePath = $directory . $fileName;
if ($this->storageCmd('exists', $filePath)) {
$this->storageCmd('delete', $filePath);
}
// Clear remote storage cache
if (!$this->isLocalStorage()) {
Cache::forget($this->getCacheKey($filePath));
}
$this->deleteEmptyDirectory($directory);
}
/**
* hasFile checks file exists on storage device
*/
protected function hasFile($fileName = null)
{
$filePath = $this->getDiskPath($fileName);
if ($this->isLocalStorage()) {
return $this->storageCmd('exists', $filePath);
}
// Cache remote storage results for performance increase
$result = Cache::memo()->rememberForever($this->getCacheKey($filePath), function() use ($filePath) {
return $this->storageCmd('exists', $filePath);
});
return $result;
}
/**
* deleteEmptyDirectory checks if directory is empty then deletes it,
* three levels up to match the partition directory.
*/
protected function deleteEmptyDirectory($dir = null)
{
if (!$this->isDirectoryEmpty($dir)) {
return;
}
$this->storageCmd('deleteDirectory', $dir);
$dir = dirname($dir);
if (!$this->isDirectoryEmpty($dir)) {
return;
}
$this->storageCmd('deleteDirectory', $dir);
$dir = dirname($dir);
if (!$this->isDirectoryEmpty($dir)) {
return;
}
$this->storageCmd('deleteDirectory', $dir);
}
/**
* isDirectoryEmpty returns true if a directory contains no files
*/
protected function isDirectoryEmpty($dir)
{
if (!$dir) {
return null;
}
return count($this->storageCmd('allFiles', $dir)) === 0;
}
//
// Storage interface
//
/**
* storageCmd calls a method against File or Storage depending on local storage
* This allows local storage outside the storage/app folder and is
* also good for performance. For local storage, *every* argument
* is prefixed with the local root path. Props to Laravel for
* the unified interface.
*/
protected function storageCmd()
{
$args = func_get_args();
$command = array_shift($args);
$result = null;
if ($this->isLocalStorage()) {
$interface = 'File';
$path = $this->getLocalRootPath();
$args = array_map(function ($value) use ($path) {
return $path . '/' . $value;
}, $args);
$result = forward_static_call_array([$interface, $command], $args);
}
else {
$result = call_user_func_array([$this->getDisk(), $command], $args);
}
return $result;
}
/**
* copyStorageToLocal file
*/
protected function copyStorageToLocal($storagePath, $localPath)
{
return FileHelper::put($localPath, $this->getDisk()->readStream($storagePath));
}
/**
* copyLocalToStorage file
*/
protected function copyLocalToStorage($localPath, $storagePath)
{
return $this->getDisk()->putFileAs(
dirname($storagePath),
$localPath,
basename($storagePath),
$this->isPublic() ? 'public' : 'private'
);
}
//
// Configuration
//
/**
* getMaxFilesize returns the maximum size of an uploaded file as configured in php.ini
* @return int The maximum size of an uploaded file in kilobytes
*/
public static function getMaxFilesize()
{
return round(UploadedFile::getMaxFilesize() / 1024);
}
/**
* getStorageDirectory defines the internal storage path, override this method
*/
public function getStorageDirectory()
{
if ($this->isPublic()) {
return 'public/';
}
return 'protected/';
}
/**
* getPublicPath defines the public address for the storage path
*/
public function getPublicPath()
{
if ($this->isPublic()) {
return 'http://localhost/storage/uploads/public/';
}
return 'http://localhost/storage/uploads/protected/';
}
/**
* getTempPath defines the internal working path, override this method
*/
public function getTempPath()
{
$path = temp_path() . '/uploads';
if (!FileHelper::isDirectory($path)) {
FileHelper::makeDirectory($path, 0755, true, true);
}
return $path;
}
/**
* getDisk returns the storage disk the file is stored on
* @return FilesystemAdapter
*/
public function getDisk()
{
return Storage::disk();
}
/**
* isLocalStorage returns true if the storage engine is local
*/
protected function isLocalStorage()
{
return Storage::getDefaultDriver() === 'local';
}
/**
* getPartitionDirectory generates a partition for the file
* return /ABC/DE1/234 for an name of ABCDE1234.
* @param Attachment $attachment
* @param string $styleName
* @return mixed
*/
protected function getPartitionDirectory()
{
return implode('/', array_slice(str_split($this->disk_name, 3), 0, 3)) . '/';
}
/**
* getLocalRootPath if working with local storage, determine the absolute local path
*/
protected function getLocalRootPath()
{
return storage_path('app/uploads');
}
}
================================================
FILE: src/Database/Attach/FileException.php
================================================
eagerLoadAttachRelation($models, $name, $constraints)) {
return $result;
}
return parent::eagerLoadRelation($models, $name, $constraints);
}
/**
* lists gets an array with the values of a given column.
* @param string $column
* @param string|null $key
* @return array
*/
public function lists($column, $key = null)
{
return $this->pluck($column, $key)->all();
}
/**
* searchWhere performs a search on this query for term found in columns.
* @param string $term Search query
* @param array $columns Table columns to search
* @param string $mode Search mode: all, any, exact.
* @return static
*/
public function searchWhere($term, $columns = [], $mode = 'all')
{
return $this->searchWhereInternal($term, $columns, $mode, 'and');
}
/**
* orSearchWhere adds an "or search where" clause to the query.
* @param string $term Search query
* @param array $columns Table columns to search
* @param string $mode Search mode: all, any, exact.
* @return static
*/
public function orSearchWhere($term, $columns = [], $mode = 'all')
{
return $this->searchWhereInternal($term, $columns, $mode, 'or');
}
/**
* searchWhereRelation performs a search on a relationship query.
*
* @param string $term Search query
* @param string $relation
* @param array $columns Table columns to search
* @param string $mode Search mode: all, any, exact.
* @return static
*/
public function searchWhereRelation($term, $relation, $columns = [], $mode = 'all')
{
return $this->whereHas($relation, function ($query) use ($term, $columns, $mode) {
$query->searchWhere($term, $columns, $mode);
});
}
/**
* orSearchWhereRelation adds an "or where" clause to a search relationship query.
* @param string $term Search query
* @param string $relation
* @param array $columns Table columns to search
* @param string $mode Search mode: all, any, exact.
* @return static
*/
public function orSearchWhereRelation($term, $relation, $columns = [], $mode = 'all')
{
return $this->orWhereHas($relation, function ($query) use ($term, $columns, $mode) {
$query->searchWhere($term, $columns, $mode);
});
}
/**
* Internal method to apply a search constraint to the query.
* Mode can be any of these options:
* - all: result must contain all words
* - any: result can contain any word
* - exact: result must contain the exact phrase
*/
protected function searchWhereInternal($term, $columns, $mode, $boolean)
{
if (!is_array($columns)) {
$columns = [$columns];
}
if (!$mode) {
$mode = 'all';
}
$grammar = $this->query->getGrammar();
if ($mode === 'exact') {
$this->where(function ($query) use ($columns, $term, $grammar) {
foreach ($columns as $field) {
if (!strlen($term)) {
continue;
}
$rawField = DbDongle::cast($grammar->wrap($field), 'TEXT');
$fieldSql = $this->query->raw(sprintf("lower(%s)", $rawField));
$termSql = '%'.trim(mb_strtolower($term)).'%';
$query->orWhere($fieldSql, 'LIKE', $termSql);
}
}, null, null, $boolean);
}
else {
$words = explode(' ', $term);
$wordBoolean = $mode === 'any' ? 'or' : 'and';
$this->where(function ($query) use ($columns, $words, $wordBoolean, $grammar) {
foreach ($columns as $field) {
$query->orWhere(function ($query) use ($field, $words, $wordBoolean, $grammar) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$rawField = DbDongle::cast($grammar->wrap($field), 'TEXT');
$fieldSql = $this->query->raw(sprintf("lower(%s)", $rawField));
$wordSql = '%'.trim(mb_strtolower($word)).'%';
$query->where($fieldSql, 'LIKE', $wordSql, $wordBoolean);
}
});
}
}, null, null, $boolean);
}
return $this;
}
/**
* paginate the given query.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)
{
// Legacy signature support
// paginate($perPage, $page, $columns, $pageName)
if (!is_array($columns)) {
$_currentPage = $columns;
$_columns = $pageName;
$_pageName = $page;
$columns = is_array($_columns) ? $_columns : ['*'];
$pageName = $_pageName !== null ? $_pageName : 'page';
$page = is_array($_currentPage) ? null : $_currentPage;
}
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = value($total) ?? $this->toBase()->getCountForPagination();
$perPage = value($perPage, $total) ?: $this->model->getPerPage();
$results = $total
? $this->forPage($page, $perPage)->get($columns)
: $this->model->newCollection();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName
]);
}
/**
* simplePaginate the given query into a simple paginator.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int $currentPage
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
// Legacy signature support
// paginate($perPage, $currentPage, $columns, $pageName)
if (!is_array($columns)) {
$_currentPage = $columns;
$_columns = $pageName;
$_pageName = $page;
$columns = is_array($_columns) ? $_columns : ['*'];
$pageName = $_pageName !== null ? $_pageName : 'page';
$page = is_array($_currentPage) ? null : $_currentPage;
}
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$this->skip(($page - 1) * $perPage)->take($perPage + 1);
return $this->simplePaginator($this->get($columns), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName
]);
}
/**
* Dynamically handle calls into the query instance.
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if ($this->model->methodExists($scope = 'scope'.ucfirst($method))) {
return $this->callScope([$this->model, $scope], $parameters);
}
return parent::__call($method, $parameters);
}
/**
* addWhereExistsQuery modifies the Laravel version to strip ORDER BY from the query,
* which is redundant in this context, also forbidden by the SQL Server driver.
*/
public function addWhereExistsQuery($query, $boolean = 'and', $not = false)
{
$query->reorder();
return parent::addWhereExistsQuery($query, $boolean, $not);
}
}
================================================
FILE: src/Database/Collection.php
================================================
pluck($value, $key)->all();
}
}
================================================
FILE: src/Database/Concerns/HasAttributes.php
================================================
getArrayableAttributes();
// Before Event
foreach ($attributes as $key => $value) {
if (($eventValue = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {
$attributes[$key] = $eventValue;
}
}
// Dates
$attributes = $this->addDateAttributesToArray($attributes);
// Mutate
$attributes = $this->addMutatedAttributesToArray(
$attributes, $mutatedAttributes = $this->getMutatedAttributes()
);
// Casts
$attributes = $this->addCastAttributesToArray(
$attributes, $mutatedAttributes
);
// Appends
foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}
// Jsonable
$attributes = $this->addJsonableAttributesToArray(
$attributes, $mutatedAttributes
);
// After Event
foreach ($attributes as $key => $value) {
if (($eventValue = $this->fireEvent('model.getAttribute', [$key, $value], true)) !== null) {
$attributes[$key] = $eventValue;
}
}
return $attributes;
}
/**
* getAttribute from the model.
* Overridden from {@link Eloquent} to implement recognition of the relation.
* @return mixed
*/
public function getAttribute($key)
{
if (
array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key) ||
$this->hasAttributeMutator($key) ||
$this->isClassCastable($key)
) {
return $this->getAttributeValue($key);
}
return $this->getRelationValue($key);
}
/**
* getRelationValue gets a relationship value from a method.
* Overridden from {@link Eloquent} to implement recognition of the relation
* using October Rain's property-based relation definitions.
* @param string $key
* @return mixed
*/
public function getRelationValue($key)
{
if ($this->relationLoaded($key)) {
return $this->relations[$key];
}
if (!$this->hasRelation($key)) {
return;
}
if ($this->attemptToAutoloadRelation($key)) {
return $this->relations[$key];
}
if ($this->preventsLazyLoading) {
$this->handleLazyLoadingViolation($key);
}
return $this->getRelationshipFromMethod($key);
}
/**
* getAttributeValue gets a plain attribute (not a relationship).
* @param string $key
* @return mixed
*/
public function getAttributeValue($key)
{
/**
* @event model.beforeGetAttribute
* Called before the model attribute is retrieved
*
* Example usage:
*
* $model->bindEvent('model.beforeGetAttribute', function ((string) $key) use (\October\Rain\Database\Model $model) {
* if ($key === 'not-for-you-to-look-at') {
* return 'you are not allowed here';
* }
* });
*
*/
if (($attr = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {
return $attr;
}
$attr = parent::getAttributeValue($key);
// Return valid json (boolean, array) if valid, otherwise
// jsonable fields will return a string for invalid data.
if ($this->isJsonable($key) && !empty($attr)) {
$_attr = json_decode($attr, true);
if (json_last_error() === JSON_ERROR_NONE) {
$attr = $_attr;
}
}
/**
* @event model.getAttribute
* Called after the model attribute is retrieved
*
* Example usage:
*
* $model->bindEvent('model.getAttribute', function ((string) $key, $value) use (\October\Rain\Database\Model $model) {
* if ($key === 'not-for-you-to-look-at') {
* return "Totally not $value";
* }
* });
*
*/
if (($_attr = $this->fireEvent('model.getAttribute', [$key, $attr], true)) !== null) {
return $_attr;
}
return $attr;
}
/**
* hasGetMutator determines if a get mutator exists for an attribute.
* @param string $key
* @return bool
*/
public function hasGetMutator($key)
{
return $this->methodExists('get'.Str::studly($key).'Attribute');
}
/**
* setAttribute sets a given attribute on the model.
* @param string $key
* @param mixed $value
* @return void
*/
public function setAttribute($key, $value)
{
// Attempting to set attribute [null] on model.
if (empty($key)) {
throw new Exception('Cannot access empty model attribute.');
}
// Handle direct relation setting
if ($this->hasRelation($key) && !$this->hasSetMutator($key)) {
return $this->setRelationSimpleValue($key, $value);
}
/**
* @event model.beforeSetAttribute
* Called before the model attribute is set
*
* Example usage:
*
* $model->bindEvent('model.beforeSetAttribute', function ((string) $key, $value) use (\October\Rain\Database\Model $model) {
* if ($key === 'do-not-touch') {
* return "$value has been touched";
* }
* });
*
*/
if (($_value = $this->fireEvent('model.beforeSetAttribute', [$key, $value], true)) !== null) {
$value = $_value;
}
// Jsonable
if ($this->isJsonable($key) && (!empty($value) || is_array($value))) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
}
// Trim strings
if ($this->trimStrings && is_string($value)) {
$value = trim($value);
}
$result = parent::setAttribute($key, $value);
/**
* @event model.setAttribute
* Called after the model attribute is set
*
* Example usage:
*
* $model->bindEvent('model.setAttribute', function ((string) $key, $value) use (\October\Rain\Database\Model $model) {
* if ($key === 'do-not-touch') {
* \Log::info("{$key} has been touched and set to {$value}!")
* }
* });
*
*/
$this->fireEvent('model.setAttribute', [$key, $value]);
return $result;
}
/**
* hasSetMutator determines if a set mutator exists for an attribute.
* @param string $key
* @return bool
*/
public function hasSetMutator($key)
{
return $this->methodExists('set'.Str::studly($key).'Attribute');
}
/**
* addCasts adds attribute casts for the model.
*
* @param array $attributes
* @return void
*/
public function addCasts($attributes)
{
$this->casts = array_merge($this->casts, $attributes);
}
/**
* getDates returns the attributes that should be converted to dates.
* @return array
*/
public function getDates()
{
if (!$this->usesTimestamps()) {
return $this->dates;
}
$defaults = [
$this->getCreatedAtColumn(),
$this->getUpdatedAtColumn(),
];
return array_unique(array_merge($this->dates, $defaults));
}
/**
* addDateAttribute adds a datetime attribute to convert to an instance
* of Carbon/DateTime object.
* @param string $attribute
* @return void
*/
public function addDateAttribute($attribute)
{
if (in_array($attribute, $this->dates)) {
return;
}
$this->dates[] = $attribute;
}
/**
* addFillable attributes for the model.
* @param array|string|null $attributes
* @return void
*/
public function addFillable($attributes = null)
{
$this->fillable = array_merge(
$this->fillable, is_array($attributes) ? $attributes : func_get_args()
);
}
/**
* addVisible attributes for the model.
* @param array|string|null $attributes
* @return void
*/
public function addVisible($attributes = null)
{
$this->visible = array_merge(
$this->visible, is_array($attributes) ? $attributes : func_get_args()
);
}
}
================================================
FILE: src/Database/Concerns/HasEagerLoadAttachRelation.php
================================================
getModel()->getRelationType($name);
if (!$relationType || !in_array($relationType, ['attachOne', 'attachMany'])) {
return null;
}
// Only vanilla attachments are supported, pass complex lookups back to Laravel
$definition = $this->getModel()->getRelationDefinition($name);
if (isset($definition['conditions']) || isset($definition['scope'])) {
return null;
}
// Opt-out of the combined eager loading logic
if (isset($definition['combineEager']) && $definition['combineEager'] === false) {
return null;
}
$relation = $this->getRelation($name);
$relatedModel = get_class($relation->getRelated());
// Perform a global look up attachment without the 'field' constraint
// to produce a combined subset of all possible attachment relations.
if (!isset($this->eagerLoadAttachResultCache[$relatedModel])) {
$relation->addCommonEagerConstraints($models);
// Note this takes first constraint only. If it becomes a problem one solution
// could be to compare the md5 of toSql() to ensure uniqueness. The workaround
// for this edge case is to set combineEager => false in the definition.
$constraints($relation);
$this->eagerLoadAttachResultCache[$relatedModel] = $relation->getEager();
}
$results = $this->eagerLoadAttachResultCache[$relatedModel];
return $relation->match(
$relation->initRelation($models, $name),
$results->where('field', $name)->values(),
$name
);
}
}
================================================
FILE: src/Database/Concerns/HasEvents.php
================================================
'beforeCreate',
'created' => 'afterCreate',
'saving' => 'beforeSave',
'saved' => 'afterSave',
'updating' => 'beforeUpdate',
'updated' => 'afterUpdate',
'deleting' => 'beforeDelete',
'deleted' => 'afterDelete',
'fetching' => 'beforeFetch',
'fetched' => 'afterFetch',
'replicating' => 'beforeReplicate',
];
foreach ($nicerEvents as $eventMethod => $method) {
self::registerModelEvent($eventMethod, function ($model) use ($method) {
$model->fireEvent("model.{$method}");
return $model->$method();
});
}
// Hooks for late stage attribute changes
self::registerModelEvent('creating', function ($model) {
$model->fireEvent('model.beforeSaveDone');
});
self::registerModelEvent('updating', function ($model) {
$model->fireEvent('model.beforeSaveDone');
});
// Boot event
$this->fireEvent('model.afterBoot');
$this->afterBoot();
static::$eventsBooted[static::class] = true;
}
/**
* initializeModelEvent is called every time the model is constructed.
*/
protected function initializeModelEvent()
{
$this->fireEvent('model.afterInit');
$this->afterInit();
}
/**
* flushEventListeners removes all of the event listeners for the model
* Also flush registry of models that had events booted
* Allows painless unit testing.
* @return void
*/
public static function flushEventListeners()
{
parent::flushEventListeners();
static::$eventsBooted = [];
}
/**
* getObservableEvents as their names.
* @return array
*/
public function getObservableEvents()
{
return array_merge(
[
'creating', 'created', 'updating', 'updated',
'deleting', 'deleted', 'saving', 'saved', 'replicating',
'trashed', 'restoring', 'restored', 'fetching', 'fetched'
],
$this->observables
);
}
/**
* fetching creates a new native event for handling beforeFetch().
* @param \Closure|string $callback
* @return void
*/
public static function fetching($callback)
{
static::registerModelEvent('fetching', $callback);
}
/**
* fetched creates a new native event for handling afterFetch().
* @param \Closure|string $callback
* @return void
*/
public static function fetched($callback)
{
static::registerModelEvent('fetched', $callback);
}
/**
* afterBoot is called after the model is constructed for the first time.
*/
protected function afterBoot()
{
/**
* @event model.afterBoot
* Called after the model is booted
*
* Example usage:
*
* $model->bindEvent('model.afterBoot', function () use (\October\Rain\Database\Model $model) {
* \Log::info(get_class($model) . ' has booted');
* });
*
*/
}
/**
* afterInit is called after the model is constructed, a nicer version
* of overriding the __construct method.
*/
protected function afterInit()
{
/**
* @event model.afterInit
* Called after the model is initialized
*
* Example usage:
*
* $model->bindEvent('model.afterInit', function () use (\October\Rain\Database\Model $model) {
* \Log::info(get_class($model) . ' has initialized');
* });
*
*/
}
/**
* beforeCreate handles the "creating" model event
*/
protected function beforeCreate()
{
/**
* @event model.beforeCreate
* Called before the model is created
*
* Example usage:
*
* $model->bindEvent('model.beforeCreate', function () use (\October\Rain\Database\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterCreate handles the "created" model event
*/
protected function afterCreate()
{
/**
* @event model.afterCreate
* Called after the model is created
*
* Example usage:
*
* $model->bindEvent('model.afterCreate', function () use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} was created!");
* });
*
*/
}
/**
* beforeUpdate handles the "updating" model event
*/
protected function beforeUpdate()
{
/**
* @event model.beforeUpdate
* Called before the model is updated
*
* Example usage:
*
* $model->bindEvent('model.beforeUpdate', function () use (\October\Rain\Database\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterUpdate handles the "updated" model event
*/
protected function afterUpdate()
{
/**
* @event model.afterUpdate
* Called after the model is updated
*
* Example usage:
*
* $model->bindEvent('model.afterUpdate', function () use (\October\Rain\Database\Model $model) {
* if ($model->title !== $model->original['title']) {
* \Log::info("{$model->name} updated its title!");
* }
* });
*
*/
}
/**
* beforeSave handles the "saving" model event
*/
protected function beforeSave()
{
/**
* @event model.beforeSave
* Called before the model is created or updated
*
* Example usage:
*
* $model->bindEvent('model.beforeSave', function () use (\October\Rain\Database\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterSave handles the "saved" model event
*/
protected function afterSave()
{
/**
* @event model.afterSave
* Called after the model is created or updated
*
* Example usage:
*
* $model->bindEvent('model.afterSave', function () use (\October\Rain\Database\Model $model) {
* if ($model->title !== $model->original['title']) {
* \Log::info("{$model->name} updated its title!");
* }
* });
*
*/
}
/**
* beforeDelete handles the "deleting" model event
*/
protected function beforeDelete()
{
/**
* @event model.beforeDelete
* Called before the model is deleted
*
* Example usage:
*
* $model->bindEvent('model.beforeDelete', function () use (\October\Rain\Database\Model $model) {
* if (!$model->isAllowedToBeDeleted()) {
* throw new \Exception("You cannot delete me!");
* }
* });
*
*/
}
/**
* afterDelete handles the "deleted" model event
*/
protected function afterDelete()
{
/**
* @event model.afterDelete
* Called after the model is deleted
*
* Example usage:
*
* $model->bindEvent('model.afterDelete', function () use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} was deleted");
* });
*
*/
}
/**
* beforeFetch handles the "fetching" model event
*/
protected function beforeFetch()
{
/**
* @event model.beforeFetch
* Called before the model is fetched
*
* Example usage:
*
* $model->bindEvent('model.beforeFetch', function () use (\October\Rain\Database\Model $model) {
* if (!\Auth::getUser()->hasAccess('fetch.this.model')) {
* throw new \Exception("You shall not pass!");
* }
* });
*
*/
}
/**
* afterFetch handles the "fetched" model event
*/
protected function afterFetch()
{
/**
* @event model.afterFetch
* Called after the model is fetched
*
* Example usage:
*
* $model->bindEvent('model.afterFetch', function () use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} was retrieved from the database");
* });
*
*/
}
/**
* beforeReplicate
*/
protected function beforeReplicate()
{
/**
* @event model.beforeReplicate
* Called as the model is replicated
*
* Example usage:
*
* $model->bindEvent('model.beforeReplicate', function () use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} is being replicated");
* });
*
*/
}
/**
* beforeRelation is fired on the relation object before it is created
*/
protected function beforeRelation($name, $relation)
{
/**
* @event model.beforeRelation
* Called when a new instance of a related model is created
*
* Example usage:
*
* $model->bindEvent('model.beforeRelation', function (string $relationName, \Illuminate\Database\Eloquent\Relations\Relation $relatedObject) use (\October\Rain\Database\Model $model) {
* // Implement scope
* $relatedObject->withLocked();
* });
*
*/
}
/**
* beforeRelation is fired on the relation model instance after it is created
*/
protected function afterRelation($name, $model)
{
/**
* @event model.afterRelation
* Called when a new instance of a related model is created
*
* Example usage:
*
* $model->bindEvent('model.afterRelation', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* // Transfer custom properties
* $relatedModel->isLocked = $model->isLocked;
* });
*
*/
}
}
================================================
FILE: src/Database/Concerns/HasJsonable.php
================================================
jsonable = array_merge($this->jsonable, $attributes);
}
/**
* isJsonable checks if an attribute is jsonable or not.
*
* @return array
*/
public function isJsonable($key)
{
return in_array($key, $this->jsonable);
}
/**
* getJsonable attributes name
*
* @return array
*/
public function getJsonable()
{
return $this->jsonable;
}
/**
* jsonable attributes set for the model.
*
* @param array $jsonable
* @return $this
*/
public function jsonable(array $jsonable)
{
$this->jsonable = $jsonable;
return $this;
}
/**
* addJsonableAttributesToArray
* @return array
*/
protected function addJsonableAttributesToArray(array $attributes, array $mutatedAttributes)
{
foreach ($this->jsonable as $key) {
if (
!array_key_exists($key, $attributes) ||
in_array($key, $mutatedAttributes)
) {
continue;
}
// Prevent double decoding of jsonable attributes.
if (!is_string($attributes[$key])) {
continue;
}
$jsonValue = json_decode($attributes[$key], true);
if (json_last_error() === JSON_ERROR_NONE) {
$attributes[$key] = $jsonValue;
}
}
return $attributes;
}
}
================================================
FILE: src/Database/Concerns/HasNicerPagination.php
================================================
paginate($perPage, ['*'], 'page', $currentPage);
}
/**
* paginateCustom paginates using a custom page name.
*
* @param int $perPage
* @param string $pageName
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginateCustom($perPage, $pageName)
{
return $this->paginate($perPage, ['*'], $pageName);
}
/**
* simplePaginateAtPage simply paginates by passing the page number directly
*
* @param int $perPage
* @param int $currentPage
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginateAtPage($perPage, $currentPage)
{
return $this->simplePaginate($perPage, ['*'], 'page', $currentPage);
}
/**
* simplePaginateCustom simply paginates using a custom page name.
*
* @param int $perPage
* @param string $pageName
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginateCustom($perPage, $pageName)
{
return $this->simplePaginate($perPage, ['*'], $pageName);
}
/**
* cursorPaginateAtPage paginates using a cursor by passing the cursor directly.
*
* @param int $perPage
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function cursorPaginateAtPage($perPage, $cursor)
{
return $this->cursorPaginate($perPage, ['*'], 'cursor', $cursor);
}
/**
* cursorPaginateCustom paginates using a cursor with a custom cursor name.
*
* @param int $perPage
* @param string $cursorName
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function cursorPaginateCustom($perPage, $cursorName)
{
return $this->cursorPaginate($perPage, ['*'], $cursorName);
}
}
================================================
FILE: src/Database/Concerns/HasRelationships.php
================================================
Item::class
* ];
* }
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait HasRelationships
{
/**
* @var array hasOne related record, inverse of belongsTo.
*
* protected $hasOne = [
* 'owner' => [User::class, 'key' => 'user_id']
* ];
*
*/
public $hasOne = [];
/**
* @var array hasMany related records, inverse of belongsTo.
*
* protected $hasMany = [
* 'items' => Item::class
* ];
*/
public $hasMany = [];
/**
* @var array belongsTo another record with a local key attribute
*
* protected $belongsTo = [
* 'parent' => [Category::class, 'key' => 'parent_id']
* ];
*/
public $belongsTo = [];
/**
* @var array belongsToMany to multiple records using a join table.
*
* protected $belongsToMany = [
* 'groups' => [Group::class, 'table'=> 'join_groups_users']
* ];
*/
public $belongsToMany = [];
/**
* @var array morphTo another record using local key and type attributes
*
* protected $morphTo = [
* 'pictures' => []
* ];
*/
public $morphTo = [];
/**
* @var array morphOne related record, inverse of morphTo.
*
* protected $morphOne = [
* 'log' => [History::class, 'name' => 'user']
* ];
*/
public $morphOne = [];
/**
* @var array morphMany related records, inverse of morphTo.
*
* protected $morphMany = [
* 'log' => [History::class, 'name' => 'user']
* ];
*/
public $morphMany = [];
/**
* @var array morphToMany to multiple records using a join table.
*
* protected $morphToMany = [
* 'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable']
* ];
*/
public $morphToMany = [];
/**
* @var array morphedByMany to a polymorphic, inverse many-to-many relationship.
*
* public $morphedByMany = [
* 'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable']
* ];
*/
public $morphedByMany = [];
/**
* @var array attachOne file attachment.
*
* protected $attachOne = [
* 'picture' => [\October\Rain\Database\Attach\File::class, 'public' => false]
* ];
*/
public $attachOne = [];
/**
* @var array attachMany file attachments.
*
* protected $attachMany = [
* 'pictures' => [\October\Rain\Database\Attach\File::class, 'name'=> 'imageable']
* ];
*/
public $attachMany = [];
/**
* @var array hasManyThrough is related records through another record.
*
* protected $hasManyThrough = [
* 'posts' => [Post::class, 'through' => User::class]
* ];
*/
public $hasManyThrough = [];
/**
* @var array hasOneThrough is a related record through another record.
*
* protected $hasOneThrough = [
* 'post' => [Post::class, 'through' => User::class]
* ];
*/
public $hasOneThrough = [];
/**
* @var array relationTypes expected, used to cycle and verify relationships.
*/
protected static $relationTypes = [
'hasOne',
'hasMany',
'belongsTo',
'belongsToMany',
'morphTo',
'morphOne',
'morphMany',
'morphToMany',
'morphedByMany',
'attachOne',
'attachMany',
'hasOneThrough',
'hasManyThrough'
];
//
// Relations
//
/**
* hasRelation checks if model has a relationship by supplied name
*/
public function hasRelation(string $name): bool
{
return $this->getRelationType($name) !== null;
}
/**
* getRelationDefinition returns relationship details from a supplied name
*/
public function getRelationDefinition(string $name): array
{
if (($type = $this->getRelationType($name)) !== null) {
return (array) $this->{$type}[$name] + $this->getRelationDefaults($type);
}
return [];
}
/**
* getRelationDefinitions returns relationship details for all relations
* defined on this model
* @return array
*/
public function getRelationDefinitions()
{
$result = [];
foreach (static::$relationTypes as $type) {
$result[$type] = $this->{$type};
// Apply default values for the relation type
if ($defaults = $this->getRelationDefaults($type)) {
foreach ($result[$type] as $relation => $options) {
$result[$type][$relation] = (array) $options + $defaults;
}
}
}
return $result;
}
/**
* getRelationType returns a relationship type based on a supplied name
* @param string $name Relation name
* @return \October\Rain\Database\Relation
*/
public function getRelationType($name)
{
foreach (static::$relationTypes as $type) {
if (isset($this->{$type}[$name])) {
return $type;
}
}
return null;
}
/**
* isRelationTypeSingular returns true if the relation is expected to return
* a single record versus a collection of records.
*/
public function isRelationTypeSingular($name): bool
{
return in_array($this->getRelationType($name), [
'hasOne',
'belongsTo',
'morphTo',
'morphOne',
'attachOne',
'hasOneThrough'
]);
}
/**
* makeRelation returns a relation class object, supporting nested relations with
* dot notation
* @param string $name
* @return \Model|null
*/
public function makeRelation($name)
{
if (str_contains($name, '.')) {
$model = $this;
$parts = explode('.', $name);
while ($relationName = array_shift($parts)) {
if (!$model = $model->makeRelation($relationName)) {
return null;
}
}
return $model;
}
$relation = $this->getRelationDefinition($name);
$relationType = $this->getRelationType($name);
if ($relationType === 'morphTo' || !isset($relation[0])) {
return null;
}
return $this->makeRelationInternal($name, $relation[0]);
}
/**
* makeRelationInternal is used internally to create a new related instance. It also
* fires the `afterRelation` to extend the created instance.
*/
protected function makeRelationInternal(string $relationName, string $relationClass)
{
$model = $this->newRelatedInstance($relationClass);
$this->fireEvent('model.afterRelation', [$relationName, $model]);
$this->afterRelation($relationName, $model);
return $model;
}
/**
* isRelationPushable determines whether the specified relation should be saved
* when `push()` is called instead of `save()` on the model. Defaults to `true`.
*/
public function isRelationPushable(string $name): bool
{
$definition = $this->getRelationDefinition($name);
if (!array_key_exists('push', $definition)) {
// @deprecated v4 this should default to false
return true;
}
return (bool) $definition['push'];
}
/**
* getRelationDefaults returns default relation arguments for a given type.
* @param string $type
* @return array
*/
protected function getRelationDefaults($type)
{
switch ($type) {
case 'attachOne':
case 'attachMany':
return ['order' => 'sort_order', 'delete' => true];
default:
return [];
}
}
/**
* handleRelation looks for the relation and does the correct magic as Eloquent would require
* inside relation methods. For more information, read the documentation of the mentioned property.
* @param string $relationName the relation key, camel-case version
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
protected function handleRelation($relationName)
{
$relationType = $this->getRelationType($relationName);
$relation = $this->getRelationDefinition($relationName);
if (!isset($relation[0]) && $relationType !== 'morphTo') {
throw new InvalidArgumentException(sprintf(
"Relation '%s' on model '%s' should have at least a classname.",
$relationName,
static::class
));
}
if (isset($relation[0]) && $relationType === 'morphTo') {
throw new InvalidArgumentException(sprintf(
"Relation '%s' on model '%s' is a morphTo relation and should not contain additional arguments.",
$relationName,
static::class
));
}
switch ($relationType) {
case 'hasOne':
case 'hasMany':
$relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']);
$relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName);
break;
case 'belongsTo':
$relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']);
$relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName);
break;
case 'belongsToMany':
$relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps']);
$relationObj = $this->$relationType($relation[0], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName);
break;
case 'morphTo':
$relation = $this->validateRelationArgs($relationName, ['name', 'type', 'id']);
$relationObj = $this->$relationType($relation['name'] ?: $relationName, $relation['type'], $relation['id']);
break;
case 'morphOne':
case 'morphMany':
$relation = $this->validateRelationArgs($relationName, ['type', 'id', 'key'], ['name']);
$relationObj = $this->$relationType($relation[0], $relation['name'], $relation['type'], $relation['id'], $relation['key'], $relationName);
break;
case 'morphToMany':
$relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']);
$relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], false, $relationName);
break;
case 'morphedByMany':
$relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']);
$relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName);
break;
case 'attachOne':
case 'attachMany':
$relation = $this->validateRelationArgs($relationName, ['public', 'key']);
$relationObj = $this->$relationType($relation[0], $relation['public'], $relation['key'], $relationName);
break;
case 'hasOneThrough':
case 'hasManyThrough':
$relation = $this->validateRelationArgs($relationName, ['key', 'throughKey', 'otherKey', 'secondOtherKey'], ['through']);
$relationObj = $this->$relationType($relation[0], $relation['through'], $relation['key'], $relation['throughKey'], $relation['otherKey'], $relation['secondOtherKey'], $relationName);
break;
default:
throw new InvalidArgumentException(sprintf("There is no such relation type known as '%s' on model '%s'.", $relationType, static::class));
}
// Relation hook event
$this->fireEvent('model.beforeRelation', [$relationName, $relationObj]);
$this->beforeRelation($relationName, $relationObj);
return $relationObj;
}
/**
* validateRelationArgs supplied relation arguments
*/
protected function validateRelationArgs($relationName, $optional, $required = [])
{
$relation = $this->getRelationDefinition($relationName);
// Query filter arguments
$filters = ['scope', 'conditions', 'order', 'pivot', 'timestamps', 'push', 'count', 'default'];
foreach (array_merge($optional, $filters) as $key) {
if (!array_key_exists($key, $relation)) {
$relation[$key] = null;
}
}
$missingRequired = [];
foreach ($required as $key) {
if (!array_key_exists($key, $relation)) {
$missingRequired[] = $key;
}
}
if ($missingRequired) {
throw new InvalidArgumentException(sprintf(
'Relation "%s" on model "%s" should contain the following key(s): %s',
$relationName,
static::class,
implode(', ', $missingRequired)
));
}
return $relation;
}
/**
* getRelationCustomClass returns a custom relation class name for
* the relation or null if none is found.
*/
protected function getRelationCustomClass(string $name): ?string
{
if (($type = $this->getRelationType($name)) !== null) {
return $this->{$type}[$name]['relationClass'] ?? null;
}
return null;
}
/**
* hasOne defines a one-to-one relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\HasOne
*/
public function hasOne($related, $primaryKey = null, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
$primaryKey = $primaryKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: HasOne::class;
return new $relationClass($instance->newQuery(), $this, $instance->getTable().'.'.$primaryKey, $localKey, $relationName);
}
/**
* morphOne defines a polymorphic one-to-one relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphOne
*/
public function morphOne($related, $name, $type = null, $id = null, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: MorphOne::class;
return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey, $relationName);
}
/**
* belongsTo defines an inverse one-to-one or many relationship. Overridden from
* \Eloquent\Model to allow the usage of the intermediary methods to handle the
* relationsData array.
* @return \October\Rain\Database\Relations\BelongsTo
*/
public function belongsTo($related, $foreignKey = null, $parentKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
if (is_null($foreignKey)) {
$foreignKey = snake_case($relationName).'_id';
}
$parentKey = $parentKey ?: $instance->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: BelongsTo::class;
return new $relationClass($instance->newQuery(), $this, $foreignKey, $parentKey, $relationName);
}
/**
* morphTo defines a polymorphic, inverse one-to-one or many relationship.
* Overridden from \Eloquent\Model to allow the usage of the intermediary
* methods to handle the relation.
* @return \October\Rain\Database\Relations\BelongsTo
*/
public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)
{
if (is_null($name)) {
$name = $this->getRelationCaller();
}
[$type, $id] = $this->getMorphs(Str::snake($name), $type, $id);
return empty($class = $this->{$type})
? $this->morphEagerTo($name, $type, $id, $ownerKey)
: $this->morphInstanceTo($class, $name, $type, $id, $ownerKey);
}
/**
* morphEagerTo defines a polymorphic, inverse one-to-one or many relationship.
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
protected function morphEagerTo($name, $type, $id, $ownerKey)
{
return new MorphTo(
$this->newQuery()->setEagerLoads([]),
$this,
$id,
$ownerKey,
$type,
$name
);
}
/**
* morphInstanceTo defines a polymorphic, inverse one-to-one or many relationship
* @param string $target
* @param string $name
* @param string $type
* @param string $id
* @param string $ownerKey
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
protected function morphInstanceTo($target, $name, $type, $id, $ownerKey)
{
$instance = $this->newRelatedInstance(
static::getActualClassNameForMorph($target)
);
return new MorphTo(
$instance->newQuery(),
$this,
$id,
$ownerKey ?? $instance->getKeyName(),
$type,
$name
);
}
/**
* hasMany defines a one-to-many relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\HasMany
*/
public function hasMany($related, $primaryKey = null, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
$primaryKey = $primaryKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: HasMany::class;
return new $relationClass($instance->newQuery(), $this, $instance->getTable().'.'.$primaryKey, $localKey, $relationName);
}
/**
* hasManyThrough defines a has-many-through relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\HasManyThrough
*/
public function hasManyThrough($related, $through, $primaryKey = null, $throughKey = null, $localKey = null, $secondLocalKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$throughInstance = new $through;
$primaryKey = $primaryKey ?: $this->getForeignKey();
$throughKey = $throughKey ?: $throughInstance->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$secondLocalKey = $secondLocalKey ?: $throughInstance->getKeyName();
$instance = $this->makeRelationInternal($relationName, $related);
$relationClass = $this->getRelationCustomClass($relationName) ?: HasManyThrough::class;
return new $relationClass($instance->newQuery(), $this, $throughInstance, $primaryKey, $throughKey, $localKey, $secondLocalKey, $relationName);
}
/**
* hasOneThrough define a has-one-through relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\HasOneThrough
*/
public function hasOneThrough($related, $through, $primaryKey = null, $throughKey = null, $localKey = null, $secondLocalKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$throughInstance = new $through;
$primaryKey = $primaryKey ?: $this->getForeignKey();
$throughKey = $throughKey ?: $throughInstance->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
$secondLocalKey = $secondLocalKey ?: $throughInstance->getKeyName();
$instance = $this->makeRelationInternal($relationName, $related);
$relationClass = $this->getRelationCustomClass($relationName) ?: HasOneThrough::class;
return new $relationClass($instance->newQuery(), $this, $throughInstance, $primaryKey, $throughKey, $localKey, $secondLocalKey, $relationName);
}
/**
* morphMany defines a polymorphic one-to-many relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphMany
*/
public function morphMany($related, $name, $type = null, $id = null, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
[$type, $id] = $this->getMorphs($name, $type, $id);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: MorphMany::class;
return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey, $relationName);
}
/**
* belongsToMany defines a many-to-many relationship.
* This code is almost a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\BelongsToMany
*/
public function belongsToMany($related, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
$primaryKey = $primaryKey ?: $this->getForeignKey();
$foreignKey = $foreignKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
$relationClass = $this->getRelationCustomClass($relationName) ?: BelongsToMany::class;
return new $relationClass(
$instance->newQuery(),
$this,
$table,
$primaryKey,
$foreignKey,
$parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(),
$relationName
);
}
/**
* morphToMany defines a polymorphic many-to-many relationship.
* This code is almost a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphToMany
*/
public function morphToMany($related, $name, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $inverse = false, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
$primaryKey = $primaryKey ?: $name.'_id';
$foreignKey = $foreignKey ?: $instance->getForeignKey();
$table = $table ?: Str::plural($name);
$relationClass = $this->getRelationCustomClass($relationName) ?: MorphToMany::class;
return new $relationClass(
$instance->newQuery(),
$this,
$name,
$table,
$primaryKey,
$foreignKey,
$parentKey ?: $this->getKeyName(),
$relatedKey ?: $instance->getKeyName(),
$relationName,
$inverse
);
}
/**
* morphedByMany defines a polymorphic many-to-many inverse relationship.
* This code is almost a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphToMany
*/
public function morphedByMany($related, $name, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$primaryKey = $primaryKey ?: $this->getForeignKey();
$foreignKey = $foreignKey ?: $name.'_id';
return $this->morphToMany(
$related,
$name,
$table,
$primaryKey,
$foreignKey,
$parentKey,
$relatedKey,
true,
$relationName
);
}
/**
* attachOne defines an attachment one-to-one relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphOne
*/
public function attachOne($related, $isPublic = true, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
[$type, $id] = $this->getMorphs('attachment', null, null);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: AttachOne::class;
return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $isPublic, $localKey, $relationName);
}
/**
* attachMany defines an attachment one-to-many relationship.
* This code is a duplicate of Eloquent but uses a Rain relation class.
* @return \October\Rain\Database\Relations\MorphMany
*/
public function attachMany($related, $isPublic = null, $localKey = null, $relationName = null)
{
if (is_null($relationName)) {
$relationName = $this->getRelationCaller();
}
$instance = $this->makeRelationInternal($relationName, $related);
[$type, $id] = $this->getMorphs('attachment', null, null);
$table = $instance->getTable();
$localKey = $localKey ?: $this->getKeyName();
$relationClass = $this->getRelationCustomClass($relationName) ?: AttachMany::class;
return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $isPublic, $localKey, $relationName);
}
/**
* getRelationCaller finds the calling function name from the stack trace.
*/
protected function getRelationCaller()
{
$backtrace = debug_backtrace(false);
$caller = $backtrace[2]['function'] === 'handleRelation'
? $backtrace[4]
: $backtrace[2];
return $caller['function'];
}
/**
* getRelationSimpleValue returns a relation key value(s), not as an object.
*/
public function getRelationSimpleValue($relationName)
{
return $this->$relationName()->getSimpleValue();
}
/**
* setRelationSimpleValue sets a relation value directly from its attribute.
*/
protected function setRelationSimpleValue($relationName, $value)
{
$this->$relationName()->setSimpleValue($value);
}
/**
* performDeleteOnRelations locates relations with delete flag and cascades the
* delete event. This is called before the parent model is deleted. This method
* checks in with the Multisite trait to preserve shared relations.
*
* @see \October\Rain\Database\Traits\Multisite::canDeleteMultisiteRelation
*/
protected function performDeleteOnRelations()
{
$definitions = $this->getRelationDefinitions();
$useMultisite = $this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) && $this->isMultisiteEnabled();
foreach ($definitions as $type => $relations) {
foreach ($relations as $name => $options) {
// Detect and preserve shared multisite relationships
if ($useMultisite && !$this->canDeleteMultisiteRelation($name, $type)) {
continue;
}
// Belongs-To-Many should clean up after itself by default
if (in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {
if (!Arr::get($options, 'detach', true)) {
continue;
}
$this->{$name}()->detach();
}
// Hard 'delete' definition
else {
if (!Arr::get($options, 'delete', false)) {
// Attachment relations should be orphaned when delete is false
if (in_array($type, ['attachOne', 'attachMany'])) {
$this->{$name}()->update([
'attachment_id' => null,
'attachment_type' => null,
'field' => null,
]);
}
continue;
}
if (!$relation = $this->{$name}) {
continue;
}
if ($relation instanceof EloquentModel) {
$relation->forceDelete();
}
elseif ($relation instanceof CollectionBase) {
$relation->each(function ($model) {
$model->forceDelete();
});
}
}
}
}
}
}
================================================
FILE: src/Database/Concerns/HasReplication.php
================================================
$this])->replicate($except);
}
/**
* duplicateWithRelations replicates a model with special multisite duplication logic.
* To avoid duplication of has many relations, the logic only propagates relations on
* the parent model since they are shared via site_root_id beyond this point.
*
* @param array|null $except
* @return static
*/
public function duplicateWithRelations(?array $except = null)
{
return App::makeWith('db.replicator', ['model' => $this])->duplicate($except);
}
/**
* newReplicationInstance returns a new instance used by the replicator
*/
public function newReplicationInstance($attributes)
{
$instance = $this->newInstance();
$instance->setRawAttributes($attributes);
$instance->fireModelEvent('replicating', false);
return $instance;
}
/**
* isRelationReplicable determines whether the specified relation should be replicated
*/
public function isRelationReplicable(string $name): bool
{
$relationType = $this->getRelationType($name);
// Skip read-only relations
if (in_array($relationType, ['morphTo', 'hasManyThrough', 'hasOneThrough'])) {
return false;
}
$definition = $this->getRelationDefinition($name);
if (!array_key_exists('replicate', $definition)) {
return true;
}
return (bool) $definition['replicate'];
}
}
================================================
FILE: src/Database/Connections/Connection.php
================================================
getQueryGrammar(),
$this->getPostProcessor()
);
}
/**
* logQuery in the connection's query log
* @param string $query
* @param array $bindings
* @param float|null $time
* @return void
*/
public function logQuery($query, $bindings, $time = null)
{
if (isset($this->events)) {
$this->events->dispatch('illuminate.query', [$query, $bindings, $time, $this->getName()]);
}
parent::logQuery($query, $bindings, $time);
}
/**
* fireConnectionEvent for this connection
* @param string $event
* @return void
*/
protected function fireConnectionEvent($event)
{
if (isset($this->events)) {
$this->events->dispatch('connection.'.$this->getName().'.'.$event, $this);
}
parent::fireConnectionEvent($event);
}
}
================================================
FILE: src/Database/Connections/ExtendsConnection.php
================================================
getQueryGrammar(),
$this->getPostProcessor()
);
}
/**
* logQuery in the connection's query log
* @param string $query
* @param array $bindings
* @param float|null $time
* @return void
*/
public function logQuery($query, $bindings, $time = null)
{
if (isset($this->events)) {
$this->events->dispatch('illuminate.query', [$query, $bindings, $time, $this->getName()]);
}
parent::logQuery($query, $bindings, $time);
}
/**
* fireConnectionEvent for this connection
* @param string $event
* @return void
*/
protected function fireConnectionEvent($event)
{
if (isset($this->events)) {
$this->events->dispatch('connection.'.$this->getName().'.'.$event, $this);
}
parent::fireConnectionEvent($event);
}
}
================================================
FILE: src/Database/Connections/MariaDbConnection.php
================================================
parseHosts($config)) as $key => $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
}
catch (PDOException $e) {
}
}
throw $e;
};
}
/**
* Create a new connection instance.
*
* @param string $driver
* @param \PDO $connection
* @param string $database
* @param string $prefix
* @param array $config
* @return \Illuminate\Database\Connection
*
* @throws \InvalidArgumentException
*/
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
if ($resolver = Connection::getResolver($driver)) {
return $resolver($connection, $database, $prefix, $config);
}
switch ($driver) {
case 'mysql':
return new MySqlConnection($connection, $database, $prefix, $config);
case 'mariadb':
return new MariaDbConnection($connection, $database, $prefix, $config);
case 'pgsql':
return new PostgresConnection($connection, $database, $prefix, $config);
case 'sqlite':
return new SQLiteConnection($connection, $database, $prefix, $config);
case 'sqlsrv':
return new SqlServerConnection($connection, $database, $prefix, $config);
}
throw new InvalidArgumentException("Unsupported driver [$driver]");
}
}
================================================
FILE: src/Database/DatabaseServiceProvider.php
================================================
registerConnectionServices();
$this->registerFakerGenerator();
$this->registerQueueableEntityResolver();
}
/**
* boot the application events
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
/**
* registerConnectionServices for the primary database bindings.
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
$this->app->bind('db.schema', function ($app) {
$builder = $app['db']->connection()->getSchemaBuilder();
// Custom blueprint resolver for schema
$builder->blueprintResolver(function ($connection, $table, $callback) {
return new Blueprint($connection, $table, $callback);
});
return $builder;
});
$this->app->singleton('db.transactions', function ($app) {
return new DatabaseTransactionsManager;
});
$this->app->bind('db.replicator', Replicator::class);
$this->app->singleton('db.dongle', function ($app) {
return new Dongle($this->getDefaultDatabaseDriver(), $app['db']);
});
$this->app->singleton('db.updater', function ($app) {
return new Updater;
});
$this->app->singleton(ConcurrencyErrorDetectorContract::class, ConcurrencyErrorDetector::class);
$this->app->singleton(LostConnectionDetectorContract::class, LostConnectionDetector::class);
}
/**
* getDefaultDatabaseDriver returns the default database driver, not just the connection name
*/
protected function getDefaultDatabaseDriver(): string
{
$defaultConnection = $this->app['db']->getDefaultConnection();
return $this->app['config']["database.connections.{$defaultConnection}.driver"];
}
}
================================================
FILE: src/Database/Dongle.php
================================================
db = $db;
$this->driver = $driver;
}
/**
* raw transforms and executes a raw SQL statement
*/
public function raw(string $sql, ?array $params = null)
{
return $this->db->raw($this->parse($sql, $params));
}
/**
* rawValue converts a raw expression to a string
*
* @todo Can be refactored if Laravel >= 10
*/
public function rawValue($sql): string
{
if (interface_exists(\Illuminate\Contracts\Database\Query\Expression::class)) {
return $this->db->raw($sql)->getValue($this->db->connection()->getQueryGrammar());
}
return (string) $this->db->raw($sql);
}
/**
* parse transforms an SQL statement to match the active driver. If params are supplied,
* replaces :column_name with array value without requiring a list of names.
* Example: custom_country_id = :country_id → custom_country_id = 7
*/
public function parse(string $sql, ?array $params = null): string
{
if (is_array($params) && preg_match_all('/\:([\w]+)/', $sql, $matches)) {
$sql = $this->parseValues($sql, $params, $matches[1]);
}
$sql = $this->parseGroupConcat($sql);
$sql = $this->parseConcat($sql);
$sql = $this->parseIfNull($sql);
$sql = $this->parseGreatest($sql);
$sql = $this->parseBooleanExpression($sql);
return $sql;
}
/**
* parseValues will protect parameter values by quoting them or handling safe values. Eg:
*
* username = :value → username = 'foobar'
* username = :value% → username = 'foobar%'
* username = %:value → username = '%foobar'
* username = %:value% → username = '%foobar%'
*/
public function parseValues(string $sql, array $data, array $paramNames)
{
$toReplace = [];
foreach ($paramNames as $param) {
$parsedValue = array_key_exists($param, $data) ? $data[$param] : null;
if (is_string($parsedValue)) {
$pdo = $this->db->getPdo();
$toReplace['%:'.$param.'%'] = $pdo->quote('%'.$parsedValue.'%');
$toReplace['%:'.$param] = $pdo->quote('%'.$parsedValue);
$toReplace[':'.$param.'%'] = $pdo->quote($parsedValue.'%');
$toReplace[':'.$param] = $pdo->quote($parsedValue);
}
else {
if (is_null($parsedValue)) {
$parsedValue = 'NULL';
}
elseif (is_numeric($parsedValue)) {
$parsedValue = +$parsedValue;
}
else {
$parsedValue = "''";
}
$toReplace['%:'.$param.'%'] = $parsedValue;
$toReplace['%:'.$param] = $parsedValue;
$toReplace[':'.$param.'%'] = $parsedValue;
$toReplace[':'.$param] = $parsedValue;
}
}
return strtr($sql, $toReplace);
}
/**
* parseGroupConcat transforms GROUP_CONCAT statement
*/
public function parseGroupConcat(string $sql): string
{
if ($this->driver === 'mysql') {
return $sql;
}
if (!str_contains(strtolower($sql), 'group_concat(')) {
return $sql;
}
$result = preg_replace_callback('/group_concat\((.+)\)/i', function ($matches) {
if (!isset($matches[1])) {
return $matches[0];
}
switch ($this->driver) {
default:
return $matches[0];
case 'pgsql':
case 'postgis':
case 'sqlite':
case 'sqlsrv':
return str_ireplace(' separator ', ', ', $matches[0]);
}
}, $sql);
if ($this->driver === 'pgsql' || $this->driver === 'postgis') {
// @todo this leaks to other definitions
$result = preg_replace("/\\(([]a-zA-Z\\-\\_\\.]+)\\,/i", "($1::VARCHAR,", $result);
$result = str_ireplace('group_concat(', 'string_agg(', $result);
}
// Requires https://groupconcat.codeplex.com/
if ($this->driver === 'sqlsrv') {
$result = str_ireplace('group_concat(', 'dbo.GROUP_CONCAT_D(', $result);
}
return $result;
}
/**
* parseConcat transforms CONCAT statement
*/
public function parseConcat(string $sql): string
{
if ($this->driver === 'mysql') {
return $sql;
}
if (!str_contains(strtolower($sql), 'concat(')) {
return $sql;
}
// Pre process special characters inside quotes
$charComma = 'X___COMMA_CHAR___X';
$result = preg_replace_callback("/'(.*?[^\\\\])'/i", function ($matches) use ($charComma) {
return str_replace(',', $charComma, $matches[0]);
}, $sql);
// Convert concat() to pipe (||) syntax
$result = preg_replace_callback('/(?:group_)?concat\((.+)\)(?R)?/i', function ($matches) {
if (!isset($matches[1])) {
return $matches[0];
}
// This is a group_concat() so ignore it
if (strpos($matches[0], 'group_') === 0) {
return $matches[0];
}
$concatFields = array_map('trim', explode(',', $matches[1]));
switch ($this->driver) {
default:
return $matches[0];
case 'pgsql':
case 'postgis':
case 'sqlite':
return implode(' || ', $concatFields);
}
}, $result);
// Replace special characters back to their originals
$result = str_replace($charComma, ',', $result);
return $result;
}
/**
* parseIfNull transforms IFNULL statement
*/
public function parseIfNull(string $sql): string
{
if ($this->driver === 'mysql') {
return $sql;
}
if (!str_contains(strtolower($sql), 'ifnull(')) {
return $sql;
}
if ($this->driver === 'pgsql' || $this->driver === 'postgis') {
return str_ireplace('ifnull(', 'coalesce(', $sql);
}
if ($this->driver === 'sqlsrv') {
return str_ireplace('ifnull(', 'isnull(', $sql);
}
return $sql;
}
/**
* parseGreatest transforms GREATEST statement for SQLite which does not
* support this function natively. Uses MAX() as a scalar replacement.
*/
public function parseGreatest(string $sql): string
{
if ($this->driver !== 'sqlite') {
return $sql;
}
if (!str_contains(strtolower($sql), 'greatest(')) {
return $sql;
}
return str_ireplace('greatest(', 'max(', $sql);
}
/**
* parseBooleanExpression transforms true|false expressions in a statement
*/
public function parseBooleanExpression(string $sql): string
{
if ($this->driver !== 'sqlite' && $this->driver !== 'sqlsrv') {
return $sql;
}
return preg_replace_callback('/(\w+)\s*(=|<>)\s*(true|false)($|\s)/i', function ($matches) {
array_shift($matches);
$space = array_pop($matches);
$matches[2] = $matches[2] === 'true' ? 1 : 0;
return implode(' ', $matches) . $space;
}, $sql);
}
/**
* cast for some drivers that require same-type comparisons
*/
public function cast(string $sql, $asType = 'INTEGER'): string
{
if ($this->driver !== 'pgsql' && $this->driver !== 'postgis') {
return $sql;
}
return 'CAST('.$sql.' AS '.$asType.')';
}
/**
* convertTimestamps alters a table's TIMESTAMP field(s) to be nullable and converts existing values.
*
* This is needed to transition from older Laravel code that set DEFAULT 0, which is an
* invalid date in newer MySQL versions where NO_ZERO_DATE is included in strict mode.
*
* @param string $table
* @param string|array $columns Column name(s). Defaults to ['created_at', 'updated_at']
*/
public function convertTimestamps($table, $columns = null)
{
if ($this->driver !== 'mysql') {
return;
}
if (!is_array($columns)) {
$columns = is_null($columns) ? ['created_at', 'updated_at'] : [$columns];
}
$prefixedTable = $this->getTablePrefix() . $table;
foreach ($columns as $column) {
$this->db->statement("ALTER TABLE {$prefixedTable} MODIFY `{$column}` TIMESTAMP NULL DEFAULT NULL");
$this->db->update("UPDATE {$prefixedTable} SET {$column} = null WHERE {$column} = 0");
}
}
/**
* disableStrictMode is used to disable strict mode during migration
*/
public function disableStrictMode()
{
if ($this->driver !== 'mysql') {
return;
}
if ($this->strictModeDisabled || $this->db->getConfig('strict') === false) {
return;
}
$this->db->statement("SET @@SQL_MODE=''");
$this->strictModeDisabled = true;
}
/**
* getDriver returns the driver name as a string, eg: pgsql
*/
public function getDriver()
{
return $this->driver;
}
/**
* getTablePrefix gets the table prefix
*/
public function getTablePrefix(): string
{
return $this->db->getTablePrefix();
}
/**
* @deprecated use parse with second argument
*/
public function parseParams(string $sql, array $params)
{
return $this->parse($sql, $params);
}
}
================================================
FILE: src/Database/ExpandoModel.php
================================================
bindEvent('model.afterFetch', [$this, 'expandoAfterFetch']);
$this->bindEvent('model.afterSave', [$this, 'expandoAfterSave']);
// Process attributes last for traits with attribute modifiers
$this->bindEvent('model.beforeSaveDone', [$this, 'expandoBeforeSaveDone'], -1);
$this->addJsonable($this->expandoColumn);
}
/**
* setExpandoAttributes on the model and protects the passthru values
*/
public function setExpandoAttributes(array $attributes = [])
{
$this->attributes = array_merge(
$this->attributes,
array_diff_key($attributes, array_flip($this->getExpandoPassthru()))
);
}
/**
* expandoAfterFetch constructor event
*/
public function expandoAfterFetch()
{
$this->attributes = array_merge((array) $this->{$this->expandoColumn}, $this->attributes);
$this->syncOriginal();
}
/**
* expandoBeforeSaveDone constructor event
*/
public function expandoBeforeSaveDone()
{
$this->{$this->expandoColumn} = array_diff_key(
$this->attributes,
array_flip($this->getExpandoPassthru())
);
$this->attributes = array_diff_key($this->attributes, $this->{$this->expandoColumn});
}
/**
* expandoAfterSave constructor event
*/
public function expandoAfterSave()
{
$this->attributes = array_merge($this->{$this->expandoColumn}, $this->attributes);
}
/**
* getExpandoPassthru
*/
protected function getExpandoPassthru()
{
$defaults = [
$this->expandoColumn,
$this->getKeyName(),
$this->getCreatedAtColumn(),
$this->getUpdatedAtColumn(),
'site_root_id',
'updated_user_id',
'created_user_id'
];
return array_merge($defaults, $this->expandoPassthru);
}
}
================================================
FILE: src/Database/Factories/Factory.php
================================================
*/
public static function factory($count = null, $state = [])
{
$factory = static::newFactory() ?: static::factoryForModel(get_called_class());
return $factory
->count(is_numeric($count) ? $count : null)
->state(is_callable($count) || is_array($count) ? $count : $state);
}
/**
* factoryForModel guesses a factory class based on the model class
*/
protected static function factoryForModel(string $modelName)
{
if (strpos($modelName, 'App\\') === 0) {
$factory = str_replace('Models\\', 'Database\\Factories\\', $modelName) . 'Factory';
}
else {
$factory = str_replace('Models\\', 'Updates\\Factories\\', $modelName) . 'Factory';
}
return $factory::new();
}
/**
* newFactory creates a new factory instance for the model.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
protected static function newFactory()
{
//
}
}
================================================
FILE: src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php
================================================
increments('id');
$table->string('master_type');
$table->string('master_field');
$table->string('slave_type');
$table->integer('slave_id');
$table->string('session_key');
$table->mediumText('pivot_data')->nullable();
$table->boolean('is_bind')->default(true);
$table->integer('sort_order')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('deferred_bindings');
}
};
================================================
FILE: src/Database/Migrations/2013_10_01_000002_Db_Files.php
================================================
increments('id');
$table->string('disk_name');
$table->string('file_name');
$table->integer('file_size');
$table->string('content_type');
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->string('field')->nullable();
$table->integer('attachment_id')->nullable();
$table->string('attachment_type')->nullable();
$table->boolean('is_public')->default(true);
$table->integer('sort_order')->nullable();
$table->timestamps();
$table->index(['attachment_type', 'attachment_id', 'field'], 'files_master_index');
});
}
public function down()
{
Schema::dropIfExists('files');
}
};
================================================
FILE: src/Database/Migrations/2015_10_01_000003_Db_Revisions.php
================================================
increments('id');
$table->string('revisionable_type');
$table->integer('revisionable_id');
$table->integer('user_id')->unsigned()->nullable()->index();
$table->string('field')->nullable()->index();
$table->string('cast')->nullable();
$table->text('old_value')->nullable();
$table->text('new_value')->nullable();
$table->timestamps();
$table->index(['revisionable_id', 'revisionable_type']);
});
}
public function down()
{
Schema::dropIfExists('revisions');
}
};
================================================
FILE: src/Database/Migrations/2026_10_01_000004_Db_Translate_Attributes.php
================================================
increments('id');
$table->string('model_type', 512);
$table->integer('model_id');
$table->string('locale', 16);
$table->string('attribute', 128);
$table->mediumText('value')->nullable();
$table->index(
['model_type', 'model_id', 'locale'],
'translate_type_id_locale_index'
);
$table->unique(
['model_type', 'model_id', 'locale', 'attribute'],
'translate_unique_index'
);
});
}
public function down()
{
Schema::dropIfExists('translate_attributes');
}
};
================================================
FILE: src/Database/Model.php
================================================
bootNicerEvents();
$this->extendableConstruct();
$this->initializeModelEvent();
$this->fill($attributes);
}
/**
* make a new model and return the instance
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model|static
*/
public static function make($attributes = [])
{
return new static($attributes);
}
/**
* create a new model and return the instance.
* @param array $attributes
* @param string $sessionKey
* @return \Illuminate\Database\Eloquent\Model|static
*/
public static function create(array $attributes = [], $sessionKey = null)
{
$model = new static($attributes);
$model->save(null, $sessionKey);
return $model;
}
/**
* reload the model attributes from the database.
* @return \Illuminate\Database\Eloquent\Model|static
*/
public function reload()
{
if (!$this->exists) {
$this->syncOriginal();
}
elseif ($fresh = static::find($this->getKey())) {
$this->setRawAttributes($fresh->getAttributes(), true);
}
return $this;
}
/**
* @deprecated use unsetRelation or unsetRelations
*/
public function reloadRelations($relationName = null)
{
if (!$relationName) {
$this->unsetRelations();
}
else {
$this->unsetRelation($relationName);
}
}
/**
* extend this object properties upon construction.
*/
public static function extend(callable $callback)
{
self::extendableExtendCallback($callback);
}
/**
* newInstance creates a new instance of the given model.
* @param array $attributes
* @param bool $exists
* @return static
*/
public function newInstance($attributes = [], $exists = false)
{
$model = parent::newInstance([], $exists);
/**
* @event model.newInstance
* Called when a new instance of a model is created
*
* Example usage:
*
* $model->bindEvent('model.newInstance', function (\October\Rain\Database\Model $newModel) use (\October\Rain\Database\Model $model) {
* // Transfer custom properties
* $newModel->isLocked = $model->isLocked;
* });
*
*/
$this->fireEvent('model.newInstance', [$model]);
// Fill last so the above event can modify fillable
$model->fill((array) $attributes);
return $model;
}
/**
* newFromBuilder creates a new model instance that is existing.
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = [], $connection = null)
{
$instance = $this->newInstance([], true);
if ($instance->fireModelEvent('fetching') === false) {
return $instance;
}
$instance->setRawAttributes((array) $attributes, true);
$instance->fireModelEvent('fetched', false);
$instance->setConnection($connection ?: $this->connection);
return $instance;
}
//
// Overrides
//
/**
* asDateTime returns a timestamp as DateTime object.
*
* @param mixed $value
* @return \Carbon\Carbon
*/
protected function asDateTime($value)
{
if ($value instanceof CarbonInterface) {
return Date::instance($value);
}
if ($value instanceof DateTimeInterface) {
return Date::parse(
$value->format('Y-m-d H:i:s.u'),
$value->getTimezone()
);
}
if (is_numeric($value)) {
return Date::createFromTimestamp($value);
}
if ($this->isStandardDateFormat($value)) {
return Date::createFromFormat('Y-m-d', $value)->startOfDay();
}
$format = $this->getDateFormat();
try {
$date = Date::createFromFormat($format, $value);
}
catch (InvalidArgumentException $ex) {
$date = false;
}
return $date ?: Date::parse($value);
}
/**
* newEloquentBuilder for the model.
* @param \October\Rain\Database\QueryBuilder $query
* @return \October\Rain\Database\Builder
*/
public function newEloquentBuilder($query)
{
return new Builder($query);
}
/**
* newBaseQueryBuilder instance for the connection.
* @return \October\Rain\Database\QueryBuilder
*/
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
$builder = new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
return $builder;
}
/**
* newCollection instance.
* @return \October\Rain\Database\Collection
*/
public function newCollection(array $models = [])
{
return new Collection($models);
}
//
// Magic
//
/**
* __get
*/
public function __get($name)
{
return $this->extendableGet($name);
}
/**
* __set
*/
public function __set($name, $value)
{
return $this->extendableSet($name, $value);
}
/**
* __call
*/
public function __call($name, $params)
{
// Never call handleRelation() anywhere else as it could
// break getRelationCaller(), use $this->{$name}() instead
if ($this->hasRelation($name)) {
return $this->handleRelation($name);
}
return $this->extendableCall($name, $params);
}
/**
* __isset determines if an attribute or relation exists on the model.
* @param string $key
* @return bool
*/
public function __isset($key)
{
return !is_null($this->getAttribute($key));
}
//
// Pivot
//
/**
* newPivot as a generic pivot model instance.
* @param \October\Rain\Database\Model $parent
* @param array $attributes
* @param string $table
* @param bool $exists
* @param string|null $using
* @return \October\Rain\Database\Pivot
*/
public function newPivot(EloquentModel $parent, array $attributes, $table, $exists, $using = null)
{
return $using
? $using::fromRawAttributes($parent, $attributes, $table, $exists)
: Pivot::fromAttributes($parent, $attributes, $table, $exists);
}
/**
* newRelationPivot instance specific to a relation.
* @param \October\Rain\Database\Model $parent
* @param string $relationName
* @param array $attributes
* @param string $table
* @param bool $exists
* @return \October\Rain\Database\Pivot
*/
public function newRelationPivot($relationName, $parent, $attributes, $table, $exists)
{
$definition = $this->getRelationDefinition($relationName);
if (!array_key_exists('pivotModel', $definition)) {
return;
}
return $this->newPivot($parent, $attributes, $table, $exists, $definition['pivotModel']);
}
//
// Saving
//
/**
* saveInternal is an internal method that saves the model to the database.
* This is used by {@link save()} and {@link forceSave()}.
* @param array $options
* @return bool
*/
protected function saveInternal($options = [])
{
$this->savingOptions = $options;
$this->sessionKey = $options['sessionKey'] ?? null;
/**
* @event model.saveInternal
* Called before the model is saved
*
* Example usage:
*
* $model->bindEvent('model.saveInternal', function ((array) $attributes, (array) $options) use (\October\Rain\Database\Model $model) {
* // Prevent anything from saving ever!
* return false;
* });
*
*/
if ($this->fireEvent('model.saveInternal', [$this->attributes, $options], true) === false) {
return false;
}
// Apply pre deferred bindings
if ($this->sessionKey !== null) {
$this->commitDeferredBefore($this->sessionKey);
}
// Save the record
$result = parent::save($options);
// Halted by event
if ($result === false) {
return $result;
}
// If there is nothing to update, Eloquent will not fire afterSave(),
// events should still fire for consistency.
if ($result === null) {
$this->fireModelEvent('updated', false);
$this->fireModelEvent('saved', false);
}
// Apply post deferred bindings
if ($this->sessionKey !== null) {
$this->commitDeferredAfter($this->sessionKey);
}
// After save deferred binding
$this->fireEvent('model.saveComplete');
return $result;
}
/**
* getSaveOption returns an option used while saving the model.
* @return mixed
*/
public function getSaveOption($key, $default = null)
{
return $this->savingOptions[$key] ?? $default;
}
/**
* save the model to the database.
* @return bool
*/
public function save(?array $options = [], $sessionKey = null)
{
return $this->saveInternal((array) $options + ['sessionKey' => $sessionKey]);
}
/**
* push saves the model and all of its relationships.
* @return bool
*/
public function push(?array $options = [], $sessionKey = null)
{
$always = Arr::get($options, 'always', false);
if (!$this->save(null, $sessionKey) && !$always) {
return false;
}
foreach ($this->relations as $name => $models) {
if (!$this->isRelationPushable($name)) {
continue;
}
if ($models instanceof CollectionBase) {
$models = $models->all();
}
elseif ($models instanceof EloquentModel) {
$models = [$models];
}
else {
$models = (array) $models;
}
foreach (array_filter($models) as $model) {
if (!$model->push(null, $sessionKey)) {
return false;
}
}
}
return true;
}
/**
* alwaysPush pushes the first level of relations even if the parent
* model has no changes.
* @return bool
*/
public function alwaysPush(?array $options = [], $sessionKey = null)
{
return $this->push(['always' => true] + (array) $options, $sessionKey);
}
/**
* performDeleteOnModel performs the actual delete query on this model instance.
*/
protected function performDeleteOnModel()
{
$this->performDeleteOnRelations();
$this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete();
}
/**
* __sleep prepare the object for serialization.
*/
public function __sleep()
{
$this->unbindEvent();
$this->extendableDestruct();
return parent::__sleep();
}
/**
* __wakeup when a model is being unserialized, check if it needs to be booted.
*/
public function __wakeup()
{
parent::__wakeup();
$this->bootNicerEvents();
$this->extendableConstruct();
$this->initializeModelEvent();
}
}
================================================
FILE: src/Database/ModelBehavior.php
================================================
model = $model;
}
}
================================================
FILE: src/Database/ModelException.php
================================================
errors());
$this->model = $model;
}
/**
* getModel returns the model with invalid attributes
*/
public function getModel(): Model
{
return $this->model;
}
}
================================================
FILE: src/Database/Models/DeferredBinding.php
================================================
findBindingRecord();
if (!$existingRecord) {
return;
}
// Remove add-delete pairs
if ((bool) $this->is_bind !== (bool) $existingRecord->is_bind) {
$existingRecord->deleteCancel();
return false;
}
// Skip repeating bindings
return false;
}
/**
* getPivotDataForBind strips attributes beginning with an underscore, allowing
* meta data to be stored using the column alongside the data.
*/
public function getPivotDataForBind($model, $relationName): array
{
$data = [];
foreach ((array) $this->pivot_data as $key => $value) {
if (str_starts_with($key, '_')) {
continue;
}
$data[$key] = $value;
}
if (
$model->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) &&
$model->isSortableRelation($relationName)
) {
$sortColumn = $model->getRelationSortOrderColumn($relationName);
$data[$sortColumn] = $this->sort_order;
}
return $data;
}
/**
* findBindingRecord finds a duplicate binding record
*/
protected function findBindingRecord()
{
return self::where('master_type', $this->master_type)
->where('master_field', $this->master_field)
->where('slave_type', $this->slave_type)
->where('slave_id', $this->slave_id)
->where('session_key', $this->session_key)
->first()
;
}
/**
* hasDeferredActions allows efficient and informed checks used by validation
*/
public static function hasDeferredActions($masterType, $sessionKey, $fieldName = null): bool
{
$cacheKey = "{$masterType}.{$sessionKey}";
if (!array_key_exists($cacheKey, self::$hasDeferredCache)) {
self::$hasDeferredCache[$cacheKey] = self::where('master_type', $masterType)
->where('session_key', $sessionKey)
->pluck('master_field')
->all()
;
}
if ($fieldName !== null) {
return in_array($fieldName, self::$hasDeferredCache[$cacheKey]);
}
return (bool) self::$hasDeferredCache[$cacheKey];
}
/**
* cancelDeferredActions cancels all deferred bindings to this model
*/
public static function cancelDeferredActions($masterType, $sessionKey)
{
$records = self::where('master_type', $masterType)
->where('session_key', $sessionKey)
->get()
;
foreach ($records as $record) {
$record->deleteCancel();
}
}
/**
* cleanUp orphan bindings
*/
public static function cleanUp($days = 5)
{
$timestamp = Carbon::now()->subDays($days)->toDateTimeString();
$records = self::where('created_at', '<', $timestamp)->get();
foreach ($records as $record) {
$record->deleteCancel();
}
}
/**
* deleteCancel deletes this binding and cancel is actions
*/
public function deleteCancel()
{
$this->deleteSlaveRecord();
$this->delete();
}
/**
* afterDelete
*/
public function afterDelete()
{
self::$hasDeferredCache = [];
}
/**
* deleteSlaveRecord is logic to cancel a binding action
*/
protected function deleteSlaveRecord()
{
if (!$this->is_bind) {
return;
}
// Try to delete unbound hasOne/hasMany records from the details table
try {
$masterType = $this->master_type;
$masterObject = new $masterType;
/**
* @event deferredBinding.newMasterInstance
* Called after the model is initialized when deleting the slave record
*
* Example usage:
*
* $model->bindEvent('deferredBinding.newMasterInstance', function ((\Model) $model) {
* if ($model instanceof MyModel) {
* $model->some_attribute = true;
* }
* });
*
*/
if (
($event = $this->fireEvent('deferredBinding.newMasterInstance', [$masterObject], true)) ||
($event = Event::fire('deferredBinding.newMasterInstance', [$this, $masterObject], true))
) {
$masterObject = $event;
}
if (!$masterObject->isDeferrable($this->master_field)) {
return;
}
$related = $masterObject->makeRelation($this->master_field);
$relatedObj = $related->find($this->slave_id);
if (!$relatedObj) {
return;
}
$options = $masterObject->getRelationDefinition($this->master_field);
if (!Arr::get($options, 'delete', false)) {
return;
}
// Only delete it if the relationship is null
$foreignKey = Arr::get($options, 'key', $masterObject->getForeignKey());
if ($foreignKey && !$relatedObj->$foreignKey) {
$relatedObj->delete();
}
}
catch (Throwable $ex) {
// Do nothing
}
}
}
================================================
FILE: src/Database/Models/Revision.php
================================================
cast === 'date') {
return $this->asDateTime($value);
}
return $value;
}
/**
* getOldValueAttribute returns "old value" casted as the saved type
*/
public function getOldValueAttribute($value)
{
if ($value === null) {
return null;
}
if ($this->cast === 'date') {
return $this->asDateTime($value);
}
return $value;
}
}
================================================
FILE: src/Database/Models/TranslateAttribute.php
================================================
[]
];
}
================================================
FILE: src/Database/MorphPivot.php
================================================
where($this->morphType, $this->morphClass);
return parent::setKeysForSaveQuery($query);
}
/**
* setKeysForSelectQuery sets the keys for a select query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function setKeysForSelectQuery($query)
{
$query->where($this->morphType, $this->morphClass);
return parent::setKeysForSelectQuery($query);
}
/**
* delete the pivot model record from the database.
*
* @return int
*/
public function delete()
{
if (isset($this->attributes[$this->getKeyName()])) {
return (int) parent::delete();
}
if ($this->fireModelEvent('deleting') === false) {
return 0;
}
$query = $this->getDeleteQuery();
$query->where($this->morphType, $this->morphClass);
return tap($query->delete(), function () {
$this->fireModelEvent('deleted', false);
});
}
/**
* getMorphType for the pivot.
*
* @return string
*/
public function getMorphType()
{
return $this->morphType;
}
/**
* setMorphType for the pivot
* @param string $morphType
* @return $this
*/
public function setMorphType($morphType)
{
$this->morphType = $morphType;
return $this;
}
/**
* setMorphClass for the pivot
* @param string $morphClass
* @return \Illuminate\Database\Eloquent\Relations\MorphPivot
*/
public function setMorphClass($morphClass)
{
$this->morphClass = $morphClass;
return $this;
}
/**
* getQueueableId for the entity.
*
* @return mixed
*/
public function getQueueableId()
{
if (isset($this->attributes[$this->getKeyName()])) {
return $this->getKey();
}
return sprintf(
'%s:%s:%s:%s:%s:%s',
$this->foreignKey, $this->getAttribute($this->foreignKey),
$this->relatedKey, $this->getAttribute($this->relatedKey),
$this->morphType, $this->morphClass
);
}
/**
* newQueryForRestoration for one or more models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
public function newQueryForRestoration($ids)
{
if (is_array($ids)) {
return $this->newQueryForCollectionRestoration($ids);
}
if (!Str::contains($ids, ':')) {
return parent::newQueryForRestoration($ids);
}
$segments = explode(':', $ids);
return $this->newQueryWithoutScopes()
->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
}
/**
* newQueryForCollectionRestoration to restore multiple models by their queueable IDs.
*
* @param array $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newQueryForCollectionRestoration(array $ids)
{
$ids = array_values($ids);
if (!Str::contains($ids[0], ':')) {
return parent::newQueryForRestoration($ids);
}
$query = $this->newQueryWithoutScopes();
foreach ($ids as $id) {
$segments = explode(':', $id);
$query->orWhere(function ($query) use ($segments) {
return $query->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
});
}
return $query;
}
}
================================================
FILE: src/Database/NestedTreeScope.php
================================================
pluck($column, $key)->all();
}
/**
* Indicate that the query results should be cached.
*
* @param \DateTime|int $minutes
* @param string $key
* @return $this
*/
public function remember($minutes, $key = null)
{
$this->cacheMinutes = $minutes;
$this->cacheKey = $key;
return $this;
}
/**
* Indicate that the query results should be cached forever.
*
* @param string $key
* @return \Illuminate\Database\Query\Builder|static
*/
public function rememberForever($key = null)
{
return $this->remember(-1, $key);
}
/**
* Indicate that the results, if cached, should use the given cache tags.
*
* @param array|mixed $cacheTags
* @return $this
*/
public function cacheTags($cacheTags)
{
$this->cacheTags = $cacheTags;
return $this;
}
/**
* @inheritDoc
*/
public function get($columns = ['*'])
{
if (!is_null($this->cacheMinutes)) {
return $this->getCached((array) $columns);
}
return parent::get($columns);
}
/**
* getCached executes the query as a cached "select" statement.
*/
public function getCached(array $columns = ['*'])
{
if (is_null($this->columns)) {
$this->columns = $columns;
}
// If the query is requested to be cached, we will cache it using a unique key
// for this database connection and query statement, including the bindings
// that are used on this query, providing great convenience when caching.
[$key, $minutes] = $this->getCacheInfo();
$cache = $this->getCache();
$callback = $this->getCacheCallback($columns);
// If the "minutes" value is less than zero, we will use that as the indicator
// that the value should be remembered values should be stored indefinitely
// and if we have minutes we will use the typical remember function here.
if ($minutes < 0) {
$results = $cache->rememberForever($key, $callback);
}
else {
$expiresAt = Carbon::now()->addMinutes($minutes);
$results = $cache->remember($key, $expiresAt, $callback);
}
return collect($results);
}
/**
* Get the cache object with tags assigned, if applicable.
*
* @return \Illuminate\Cache\CacheManager
*/
protected function getCache()
{
$cache = App::make('cache');
return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache;
}
/**
* getCacheInfo returns key and cache minutes
*/
protected function getCacheInfo(): array
{
return [$this->getCacheKey(), $this->cacheMinutes];
}
/**
* getCacheKey returns a unique cache key for the complete query
*/
public function getCacheKey(): string
{
return $this->cacheKey ?: $this->generateCacheKey();
}
/**
* Generate the unique cache key for the query.
*
* @return string
*/
public function generateCacheKey()
{
$name = $this->connection->getName();
return md5($name.$this->toSql().serialize($this->getBindings()));
}
/**
* Get the Closure callback used when caching queries.
*
* @param array $columns
* @return \Closure
*/
protected function getCacheCallback($columns)
{
return function () use ($columns) {
return parent::get($columns)->all();
};
}
/**
* Retrieve the "count" result of the query,
* also strips off any orderBy clause.
*
* @param string $columns
* @return int
*/
public function count($columns = '*')
{
$previousOrders = $this->orders;
$this->orders = null;
$result = parent::count($columns);
$this->orders = $previousOrders;
return $result;
}
}
================================================
FILE: src/Database/README.md
================================================
## Rain Database
The October Rain Foundation is an extension of the Eloquent ORM used by Laravel. It adds the following features:
### Usage Instructions
See the [Illuminate Database instructions](https://github.com/illuminate/database/blob/master/README.md) for usage outside the Laravel framework.
### Alternate relations and events
Relations and events can be defined using an alternative syntax, which is preferred by the [October CMS platform](http://octobercms.com).
[See October CMS Model documentation](https://octobercms.com/docs/database/model)
### Model validation
Models can define validation rules Laravel's built-in Validator class.
[See October CMS Model documentation](https://octobercms.com/docs/database/model)
### Deferred bindings
Deferred bindings allow you to postpone model relationships until the master record commits the changes. This is particularly useful if you need to prepare some models (such as file uploads) and associate them to another model that doesn't exist yet.
[See Deferred binding documentation](https://octobercms.com/docs/database/relations#deferred-binding)
### Tree Trait Interface
Traits do not support interfaces so this cannot be executed in the code. These are the expectations of a "Tree" trait, currently: NestedTree, SimpleTree.
These methods should support query builder chaining, i.e defined as scopes:
- getAllRoot(): Return just the root nodes.
- getNested(): Return all nodes with the `children` relationship eager loaded.
- listsNested(): Returns a key, value array of records, where values are indented based on their level.
These methods do not require chaining:
- getChildren(): Return the child nodes below this one.
- getChildCount(): Return the number of children below this node.
All models must return a collection of the base class `October\Rain\Database\TreeCollection`.
================================================
FILE: src/Database/Relations/AttachMany.php
================================================
relationName = $relationName;
$this->public = $isPublic;
parent::__construct($query, $parent, $type, $id, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
* @param mixed $value
* @return void
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
$this->parent->unsetRelation($this->relationName);
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function() {
$this->ensureRelationIsEmpty();
});
}
return;
}
// Append a single newly uploaded file(s)
if ($value instanceof UploadedFile) {
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$this->create(['data' => $value]);
});
return;
}
// Append existing File model
if ($value instanceof FileModel) {
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$this->add($value);
});
return;
}
// Process multiple values
$files = $models = $keys = [];
if (is_array($value)) {
foreach ($value as $_value) {
if ($_value instanceof UploadedFile) {
$files[] = $_value;
}
elseif ($_value instanceof FileModel) {
$models[] = $_value;
}
elseif (is_numeric($_value)){
$keys[] = $_value;
}
}
}
if ($files) {
$this->parent->bindEventOnce('model.afterSave', function () use ($files) {
foreach ($files as $file) {
$this->create(['data' => $file]);
}
});
}
if ($keys) {
$this->parent->bindEventOnce('model.afterSave', function () use ($keys) {
$models = $this->getRelated()
->whereIn($this->getRelatedKeyName(), (array) $keys)
->get()
;
foreach ($models as $model) {
$this->add($model);
}
});
}
if ($models) {
$this->parent->bindEventOnce('model.afterSave', function () use ($models) {
foreach ($models as $model) {
$this->add($model);
}
});
}
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
* @return array|null
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($this->parent->relationLoaded($relationName)) {
$files = $this->parent->getRelation($relationName);
}
else {
$files = $this->getResults();
}
if ($files) {
$value = [];
foreach ($files as $file) {
$value[] = $file->getKey();
}
}
return $value;
}
}
================================================
FILE: src/Database/Relations/AttachOne.php
================================================
relationName = $relationName;
$this->public = $isPublic;
parent::__construct($query, $parent, $type, $id, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
* @param mixed $value
* @return void
*/
public function setSimpleValue($value)
{
if (is_array($value)) {
$value = current($value);
}
// Nulling the relationship
if (!$value) {
$this->parent->setRelation($this->relationName, null);
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function() {
$this->ensureRelationIsEmpty();
});
}
return;
}
// Newly uploaded file
if ($value instanceof UploadedFile) {
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$file = $this->create(['data' => $value]);
$this->parent->setRelation($this->relationName, $file);
});
}
// Existing File model
elseif ($value instanceof FileModel) {
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$this->add($value);
});
}
// Model key
elseif (is_numeric($value)) {
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
if ($model = $this->getRelated()->find($value)) {
$this->add($model);
}
});
}
// The relation is set here to satisfy validation
$this->parent->setRelation($this->relationName, $value);
$this->parent->bindEventOnce('model.afterValidate', function() {
$this->parent->unsetRelation($this->relationName);
});
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
* @return string|null
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($this->parent->relationLoaded($relationName)) {
$file = $this->parent->getRelation($relationName);
}
else {
$file = $this->getResults();
}
if ($file) {
$value = $file->getKey();
}
return $value;
}
}
================================================
FILE: src/Database/Relations/AttachOneOrMany.php
================================================
public) && $this->public !== null) {
return $this->public;
}
return true;
}
/**
* addConstraints sets the field (relation name) constraint on the query
* @return void
*/
public function addConstraints()
{
if (static::$constraints) {
$this->query->where($this->morphType, $this->morphClass);
$this->query->where($this->foreignKey, '=', $this->getParentKey());
$this->query->where('field', $this->relationName);
$this->query->whereNotNull($this->foreignKey);
}
}
/**
* getRelationExistenceQuery adds the constraints for a relationship count query
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if ($parentQuery->getQuery()->from === $query->getQuery()->from) {
$query = $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns);
}
else {
$query = $query->select($columns)->whereColumn($this->getExistenceCompareKey(), '=', $this->getQualifiedParentKeyName());
}
$query = $query->where($this->morphType, $this->morphClass);
return $query->where('field', $this->relationName);
}
/**
* getRelationExistenceQueryForSelfRelation adds the constraints for a relationship query on the same table
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query->select($columns)->from(
$query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()
);
$query->getModel()->setTable($hash);
return $query->whereColumn($hash.'.'.$this->getForeignKeyName(), '=', $this->getQualifiedParentKeyName());
}
/**
* addEagerConstraints sets the field constraint for an eager load of the relation
* @param array $models
* @return void
*/
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
$this->query->where('field', $this->relationName);
}
/**
* addCommonEagerConstraints adds constraints without the field constraint, used to
* eager load multiple relations of a common type.
* @see \October\Rain\Database\Concerns\HasEagerLoadAttachRelation
* @param array $models
* @return void
*/
public function addCommonEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
}
/**
* save the supplied related model
*/
public function save(Model $model, $sessionKey = null)
{
if (!array_key_exists('is_public', $model->attributes)) {
$model->setAttribute('is_public', $this->isPublic());
}
$model->setAttribute('field', $this->relationName);
if ($sessionKey === null) {
$this->ensureAttachOneIsSingular();
return parent::save($model);
}
$this->add($model, $sessionKey);
return $model->save() ? $model : false;
}
/**
* saveQuietly saves the supplied related model without raising any events.
*/
public function saveQuietly(Model $model, $sessionKey = null)
{
return Model::withoutEvents(function () use ($model, $sessionKey) {
return $this->save($model, $sessionKey);
});
}
/**
* saveMany saves multiple models with deferred binding support.
*/
public function saveMany($models, $sessionKey = null)
{
foreach ($models as $model) {
$this->save($model, $sessionKey);
}
return $models;
}
/**
* saveManyQuietly saves multiple models without raising any events.
*/
public function saveManyQuietly($models, $sessionKey = null)
{
return Model::withoutEvents(function () use ($models, $sessionKey) {
return $this->saveMany($models, $sessionKey);
});
}
/**
* create a new instance of this related model
*/
public function create(array $attributes = [], $sessionKey = null)
{
if (!array_key_exists('is_public', $attributes)) {
$attributes = array_merge(['is_public' => $this->isPublic()], $attributes);
}
$attributes['field'] = $this->relationName;
if ($sessionKey === null) {
$this->ensureAttachOneIsSingular();
}
$model = parent::create($attributes);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* createQuietly creates a new instance without raising any events.
*/
public function createQuietly(array $attributes = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $sessionKey) {
return $this->create($attributes, $sessionKey);
});
}
/**
* createMany creates multiple instances of related models.
*/
public function createMany(iterable $records, $sessionKey = null)
{
$instances = [];
foreach ($records as $record) {
$instances[] = $this->create($record, $sessionKey);
}
return $instances;
}
/**
* createManyQuietly creates multiple instances without raising any events.
*/
public function createManyQuietly(iterable $records, $sessionKey = null)
{
return Model::withoutEvents(function () use ($records, $sessionKey) {
return $this->createMany($records, $sessionKey);
});
}
/**
* createFromFile
*/
public function createFromFile(string $filePath, array $attributes = [], $sessionKey = null)
{
if (!array_key_exists('is_public', $attributes)) {
$attributes = array_merge(['is_public' => $this->isPublic()], $attributes);
}
$attributes['field'] = $this->relationName;
if ($sessionKey === null) {
$this->ensureAttachOneIsSingular();
}
$model = parent::make($attributes);
$model->fromFile($filePath);
$model->save();
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* add a model to this relationship type
*/
public function add(Model $model, $sessionKey = null)
{
if (!array_key_exists('is_public', $model->attributes)) {
$model->is_public = $this->isPublic();
}
if ($sessionKey === null) {
/**
* @event model.relation.beforeAdd
* Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'some_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {
return;
}
$this->ensureAttachOneIsSingular();
// Associate the model
if ($this->parent->exists) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->setAttribute($this->getMorphType(), $this->morphClass);
$model->setAttribute('field', $this->relationName);
$model->save();
}
else {
$this->parent->bindEventOnce('model.afterSave', function () use ($model) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->setAttribute($this->getMorphType(), $this->morphClass);
$model->setAttribute('field', $this->relationName);
$model->save();
});
}
// Use the opportunity to set the relation in memory
if ($this instanceof AttachOne) {
$this->parent->setRelation($this->relationName, $model);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.add
* Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.add', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);
}
else {
$this->ensureAttachOneIsSingular($sessionKey);
$this->parent->bindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* addMany attaches an array of models to the parent instance with deferred binding support
* @param array $models
*/
public function addMany($models, $sessionKey = null)
{
foreach ($models as $model) {
$this->add($model, $sessionKey);
}
}
/**
* remove a model from this relationship type
*/
public function remove(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
/**
* @event model.relation.beforeRemove
* Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'perm_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {
return;
}
if (!$this->isModelRemovable($model)) {
return;
}
$options = $this->parent->getRelationDefinition($this->relationName);
if (Arr::get($options, 'delete', false)) {
$model->delete();
}
else {
// Make this model an orphan ;~(
$model->setAttribute($this->getForeignKeyName(), null);
$model->setAttribute($this->getMorphType(), null);
$model->setAttribute('field', null);
$model->save();
}
// Use the opportunity to set the relation in memory
if ($this instanceof AttachOne) {
$this->parent->setRelation($this->relationName, null);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.remove
* Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.remove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was removed from {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);
}
else {
$this->parent->unbindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* isModelRemovable returns true if an existing model is already associated
*/
protected function isModelRemovable($model): bool
{
return
((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey()) &&
$model->getAttribute($this->getMorphType()) === $this->morphClass &&
$model->getAttribute('field') === $this->relationName;
}
/**
* ensureAttachOneIsSingular ensures AttachOne only has one attachment,
* by deleting siblings for singular relations.
*/
protected function ensureAttachOneIsSingular($sessionKey = null)
{
if (!$this instanceof AttachOne) {
return;
}
if ($sessionKey) {
foreach ($this->withDeferred($sessionKey)->get() as $record) {
$this->parent->unbindDeferred($this->relationName, $record, $sessionKey);
}
return;
}
if ($this->parent->exists) {
$this->delete();
}
}
/**
* @deprecated this method is removed in October CMS v4
*/
public function makeValidationFile($value)
{
if ($value instanceof FileModel) {
$localPath = $value->getLocalPath();
// Exception handling for UploadedFile
if (file_exists($localPath)) {
return new UploadedFile(
$localPath,
$value->file_name,
$value->content_type,
null,
true
);
}
// Fallback to string
$value = $localPath;
}
/*
* @todo `$value` might be a string, may not validate
*/
return $value;
}
/**
* ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.
*/
protected function ensureRelationIsEmpty()
{
$options = $this->parent->getRelationDefinition($this->relationName);
if (Arr::get($options, 'delete', false)) {
$this->delete();
}
else {
$this->update([$this->getForeignKeyName() => null]);
}
}
/**
* getRelatedKeyName
* @return string
*/
public function getRelatedKeyName()
{
return $this->related->getKeyName();
}
/**
* @deprecated use getForeignKeyName
*/
public function getForeignKey()
{
return $this->foreignKey;
}
/**
* @deprecated use getLocalKeyName
*/
public function getOtherKey()
{
return $this->localKey;
}
}
================================================
FILE: src/Database/Relations/BelongsTo.php
================================================
relationName = $relationName;
parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName);
$this->addDefinedConstraints();
}
/**
* create a new instance of this related model with deferred binding support
*/
public function create(array $attributes = [], $sessionKey = null)
{
$model = parent::create($attributes);
$this->add($model, $sessionKey);
return $model;
}
/**
* add a model to this relationship type.
*/
public function add(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
$this->associate($model);
}
else {
$this->child->bindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* remove a model from this relationship type.
*/
public function remove(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
$this->dissociate();
}
else {
$this->child->unbindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* associate the model instance to the given parent.
*/
public function associate($model)
{
/**
* @event model.relation.beforeAssociate
* Called before associating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAssociate', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'some_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAssociate', [$this->relationName, $model], true) === false) {
return;
}
$result = parent::associate($model);
/**
* @event model.relation.associate
* Called after associating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.associate', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was associated as {$relationName} to {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.associate', [$this->relationName, $model]);
return $result;
}
/**
* dissociate previously dissociated model from the given parent.
*/
public function dissociate()
{
/**
* @event model.relation.beforeDissociate
* Called before dissociating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'perm_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName], true) === false) {
return;
}
$result = parent::dissociate();
/**
* @event model.relation.dissociate
* Called after dissociating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.dissociate', function (string $relationName) use (\October\Rain\Database\Model $model) {
* $modelClass = get_class($model);
* traceLog("{$relationName} was dissociated from {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.dissociate', [$this->relationName]);
return $result;
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
* @param mixed $value
* @return void
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
$this->dissociate();
return;
}
if ($value instanceof Model) {
// Non existent model, use a single serve event to associate it again when ready
if (!$value->exists) {
$value->bindEventOnce('model.afterSave', function () use ($value) {
$this->associate($value);
});
}
$this->associate($value);
$this->child->setRelation($this->relationName, $value);
}
else {
$this->child->setAttribute($this->getForeignKeyName(), $value);
$this->child->unsetRelation($this->relationName);
}
}
/**
* getSimpleValue is a helper for getting this relationship simple value,
* generally useful with form values.
* @return string|null
*/
public function getSimpleValue()
{
return $this->child->getAttribute($this->getForeignKeyName());
}
/**
* @deprecated use getOwnerKeyName
*/
public function getOtherKey()
{
return $this->ownerKey;
}
}
================================================
FILE: src/Database/Relations/BelongsToMany.php
================================================
addDefinedConstraints();
}
/**
* addWhereConstraints sets the where clause for the relation query.
* @return $this
*/
protected function addWhereConstraints()
{
parent::addWhereConstraints();
$this->addPivotSiteScopeConstraints();
return $this;
}
/**
* addEagerConstraints sets the constraints for an eager load of the relation.
* @param array $models
* @return void
*/
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
$this->addPivotSiteScopeConstraints();
}
/**
* baseAttachRecord creates a new pivot attachment record.
* @param int $id
* @param bool $timed
* @return array
*/
protected function baseAttachRecord($id, $timed)
{
$record = parent::baseAttachRecord($id, $timed);
if ($siteId = $this->getPivotSiteScopeValue()) {
$record['site_id'] = $siteId;
}
return $record;
}
/**
* getRelationExistenceQuery adds the constraints for a relationship count query.
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
if ($this->pivotSiteScope) {
$query->where($this->qualifyPivotColumn('site_id'), Site::getSiteIdFromContext());
}
return $query;
}
/**
* newPivotQuery creates a new query builder for the pivot table.
*/
public function newPivotQuery()
{
$query = parent::newPivotQuery();
if ($this->pivotSiteScope) {
$query->where($this->table.'.site_id', Site::getSiteIdFromContext());
}
return $query;
}
/**
* save the supplied related model with deferred binding support.
*/
public function save(Model $model, array $pivotData = [], $sessionKey = null)
{
$model->save();
$this->add($model, $sessionKey, $pivotData);
return $model;
}
/**
* saveQuietly saves the model without raising any events,
* with deferred binding support.
*/
public function saveQuietly(Model $model, array $pivotData = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($model, $pivotData, $sessionKey) {
return $this->save($model, $pivotData, $sessionKey);
});
}
/**
* saveMany saves multiple models with deferred binding support.
*/
public function saveMany($models, array $pivotData = [], $sessionKey = null)
{
foreach ($models as $model) {
$this->save($model, $pivotData, $sessionKey);
}
return $models;
}
/**
* saveManyQuietly saves multiple models without raising any events,
* with deferred binding support.
*/
public function saveManyQuietly($models, array $pivotData = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($models, $pivotData, $sessionKey) {
return $this->saveMany($models, $pivotData, $sessionKey);
});
}
/**
* create a new instance of this related model with deferred binding support.
*/
public function create(array $attributes = [], array $pivotData = [], $sessionKey = null)
{
$model = $this->related->create($attributes);
$this->add($model, $sessionKey, $pivotData);
return $model;
}
/**
* createQuietly creates a new instance without raising any events,
* with deferred binding support.
*/
public function createQuietly(array $attributes = [], array $pivotData = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $pivotData, $sessionKey) {
return $this->create($attributes, $pivotData, $sessionKey);
});
}
/**
* createMany creates multiple related models with deferred binding support.
*/
public function createMany(iterable $records, array $pivotData = [], $sessionKey = null)
{
$instances = $this->related->newCollection();
foreach ($records as $record) {
$instances->push($this->create($record, $pivotData, $sessionKey));
}
return $instances;
}
/**
* createManyQuietly creates multiple models without raising any events,
* with deferred binding support.
*/
public function createManyQuietly(iterable $records, array $pivotData = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($records, $pivotData, $sessionKey) {
return $this->createMany($records, $pivotData, $sessionKey);
});
}
/**
* createOrFirst attempts to create the record, or if a unique constraint
* violation occurs, finds the existing record.
*/
public function createOrFirst(array $attributes = [], \Closure|array $values = [], array $pivotData = [], $sessionKey = null)
{
$model = $this->related->createOrFirst($attributes, $values);
$this->add($model, $sessionKey, $pivotData);
return $model;
}
/**
* attach overrides attach() method of BelongToMany relation
* This is necessary in order to fire 'model.relation.beforeAttach', 'model.relation.attach' events
* @param mixed $ids
* @param array $attributes
* @param bool $touch
*/
public function attach($ids, array $attributes = [], $touch = true)
{
// Normalize identifiers for events, this occurs internally in the parent logic
// and should have no cascading effects.
$parsedIds = $this->parseIds($ids);
/**
* @event model.relation.beforeAttach
* Called before creating a new relation between models (only for BelongsToMany relation)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAttach', function (string $relationName, array $ids, array $attributes) use (\October\Rain\Database\Model $model) {
* foreach ($ids as $id) {
* if (!$model->isRelationValid($id)) {
* return false;
* }
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, &$parsedIds, &$attributes], true) === false) {
return;
}
/*
* See \Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable
*/
parent::attach($parsedIds, $attributes, $touch);
/**
* @event model.relation.attach
* Called after creating a new relation between models (only for BelongsToMany relation)
*
* Example usage:
*
* $model->bindEvent('model.relation.attach', function (string $relationName, array $ids, array $attributes) use (\October\Rain\Database\Model $model) {
* foreach ($ids as $id) {
* traceLog("New relation {$relationName} was created", $id);
* }
* });
*
*/
$this->parent->fireEvent('model.relation.attach', [$this->relationName, $parsedIds, $attributes]);
}
/**
* detach overrides detach() method of BelongToMany relation.
* This is necessary in order to fire 'model.relation.beforeDetach', 'model.relation.detach' events
* @param mixed $ids
* @param bool $touch
* @return int|void
*/
public function detach($ids = null, $touch = true)
{
// Normalize identifiers for events, this occurs internally in the parent logic
// and should have no cascading effects. Null is used to detach everything.
$parsedIds = $ids !== null ? $this->parseIds($ids) : $ids;
/**
* @event model.relation.beforeDetach
* Called before removing a relation between models (only for BelongsToMany relation)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeDetach', function (string $relationName, ?array $parsedIds) use (\October\Rain\Database\Model $model) {
* foreach ((array) $parsedIds as $id) {
* if (!$model->isRelationValid($parsedIds)) {
* return false;
* }
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeDetach', [$this->relationName, &$parsedIds], true) === false) {
return;
}
/*
* See \Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable
*/
$results = parent::detach($parsedIds, $touch);
/**
* @event model.relation.detach
* Called after removing a relation between models (only for BelongsToMany relation)
*
* Example usage:
*
* $model->bindEvent('model.relation.detach', function (string $relationName, ?array $parsedIds, int $results) use (\October\Rain\Database\Model $model) {
* foreach ($ids as $id) {
* traceLog("Relation {$relationName} was removed", (array) $parsedIds);
* }
* });
*
*/
$this->parent->fireEvent('model.relation.detach', [$this->relationName, $parsedIds, $results]);
}
/**
* add a model to this relationship type.
*/
public function add(Model $model, $sessionKey = null, $pivotData = [])
{
if (is_array($sessionKey)) {
$pivotData = $sessionKey;
$sessionKey = null;
}
// Associate the model
if ($sessionKey === null) {
if ($this->parent->exists) {
$this->attach($model, $pivotData);
}
else {
$this->parent->bindEventOnce('model.afterSave', function () use ($model, $pivotData) {
$this->attach($model, $pivotData);
});
}
$this->parent->unsetRelation($this->relationName);
}
else {
$this->parent->bindDeferred($this->relationName, $model, $sessionKey, $pivotData);
}
}
/**
* remove a model from this relationship type.
*/
public function remove(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
$this->detach($model);
$this->parent->unsetRelation($this->relationName);
}
else {
$this->parent->unbindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* paginate gets a paginator for the "select" statement that complies with October Rain
*
* @param int $perPage
* @param int $currentPage
* @param array $columns
* @param string $pageName
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $currentPage = null)
{
// Legacy signature support
// paginate($perPage, $currentPage, $columns, $pageName)
if (!is_array($columns)) {
$_currentPage = $columns;
$_columns = $pageName;
$_pageName = $currentPage;
$columns = is_array($_columns) ? $_columns : ['*'];
$pageName = $_pageName !== null ? $_pageName : 'page';
$currentPage = is_array($_currentPage) ? null : $_currentPage;
}
$this->query->addSelect($this->shouldSelect($columns));
$paginator = $this->query->paginate($perPage, $currentPage, $columns);
$this->hydratePivotRelation($paginator->items());
return $paginator;
}
/**
* simplePaginate using a simple paginator.
*
* @param int|null $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $currentPage = null)
{
// Legacy signature support
// paginate($perPage, $currentPage, $columns, $pageName)
if (!is_array($columns)) {
$_currentPage = $columns;
$_columns = $pageName;
$_pageName = $currentPage;
$columns = is_array($_columns) ? $_columns : ['*'];
$pageName = $_pageName !== null ? $_pageName : 'page';
$currentPage = is_array($_currentPage) ? null : $_currentPage;
}
$this->query->addSelect($this->shouldSelect($columns));
$paginator = $this->query->simplePaginate($perPage, $currentPage, $columns);
$this->hydratePivotRelation($paginator->items());
return $paginator;
}
/**
* cursorPaginate using a cursor paginator.
*
* @param int|null $perPage
* @param array $columns
* @param string $cursorName
* @param \Illuminate\Pagination\Cursor|string|null $cursor
* @return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
$this->query->addSelect($this->shouldSelect($columns));
$paginator = $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor);
$this->hydratePivotRelation($paginator->items());
return $paginator;
}
/**
* newPivot creates a new pivot model instance
*
* @param array $attributes
* @param bool $exists
* @return \Illuminate\Database\Eloquent\Relations\Pivot
*/
public function newPivot(array $attributes = [], $exists = false)
{
// October looks to the relationship parent
$pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists);
// Laravel looks to the related model
if (empty($pivot)) {
$pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists, $this->using);
}
return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
// Disassociate in memory immediately
$this->parent->setRelation(
$this->relationName,
$this->getRelated()->newCollection()
);
// Perform sync when the model is saved
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$this->detach();
});
return;
}
// Convert models to keys
if ($value instanceof Model) {
$value = $value->{$this->getRelatedKeyName()};
}
elseif (is_array($value)) {
foreach ($value as $_key => $_value) {
if ($_value instanceof Model) {
$value[$_key] = $_value->{$this->getRelatedKeyName()};
}
}
}
// Setting the relationship
$relationCollection = $value instanceof CollectionBase
? $value
: $this->newSimpleRelationQuery((array) $value)->get();
// Associate in memory immediately
$this->parent->setRelation($this->relationName, $relationCollection);
// Perform sync when the model is saved
$this->parent->bindEventOnce('model.afterSave', function () use ($value) {
$this->sync($value);
});
}
/**
* newSimpleRelationQuery for the related instance based on an array of IDs.
*/
protected function newSimpleRelationQuery(array $ids)
{
$model = $this->getRelated();
$query = $model->newQuery();
return $query->whereIn($this->getRelatedKeyName(), $ids);
}
/**
* getSimpleValue is a helper for getting this relationship simple value,
* generally useful with form values
*/
public function getSimpleValue()
{
$value = [];
$relationName = $this->relationName;
if ($this->parent->relationLoaded($relationName)) {
$value = $this->parent->getRelation($relationName)
->pluck($this->getRelatedKeyName())
->all()
;
}
else {
$value = $this->allRelatedIds()->all();
}
return $value;
}
/**
* @deprecated use getQualifiedForeignPivotKeyName
*/
public function getForeignKey()
{
return $this->table.'.'.$this->foreignPivotKey;
}
/**
* @deprecated use getQualifiedRelatedPivotKeyName
*/
public function getOtherKey()
{
return $this->table.'.'.$this->relatedPivotKey;
}
/**
* shouldSelect gets the select columns for the relation query
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
protected function shouldSelect(array $columns = ['*'])
{
// @deprecated remove this whole method when `countMode` is gone
if ($this->countMode) {
return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey;
}
if ($columns === ['*']) {
$columns = [$this->related->getTable().'.*'];
}
return array_merge($columns, $this->aliasedPivotColumns());
}
/**
* performJoin will join the pivot table opportunistically instead of mandatorily
* to support deferred bindings that exist in another table.
*
* This method is based on `performJoin` method logic except it uses a left join.
*
* @param \Illuminate\Database\Eloquent\Builder|null $query
* @return $this
*/
protected function performLeftJoin($query = null)
{
$query = $query ?: $this->query;
$query->leftJoin($this->table, function($join) {
$join->on($this->getQualifiedRelatedKeyName(), '=', $this->getQualifiedRelatedPivotKeyName());
$join->where($this->getQualifiedForeignPivotKeyName(), $this->parent->getKey());
});
return $this;
}
/**
* performSortableColumnJoin includes custom logic to replace the sort order column with
* a unified column
*/
protected function performSortableColumnJoin($query = null, $sessionKey = null)
{
if (
!$this->parent->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) ||
!$this->parent->isSortableRelation($this->relationName)
) {
return;
}
// Check if sorting by the matched sort_order column
$sortColumn = $this->qualifyPivotColumn(
$this->parent->getRelationSortOrderColumn($this->relationName)
);
$orderDefinitions = $query->getQuery()->orders;
if (!is_array($orderDefinitions)) {
return;
}
$sortableIndex = false;
foreach ($orderDefinitions as $index => $order) {
if ($order['column'] === $sortColumn) {
$sortableIndex = $index;
}
}
// Not sorting by the sort column, abort
if ($sortableIndex === false) {
return;
}
// Join the deferred binding table and select the combo column
$tempOrderColumns = 'october_reserved_sort_order';
$combinedOrderColumn = "ifnull(deferred_bindings.sort_order, {$sortColumn}) as {$tempOrderColumns}";
$this->performDeferredLeftJoin($query, $sessionKey);
$this->addSelect(DbDongle::raw($combinedOrderColumn));
// Overwrite the sortable column with the combined one
$query->getQuery()->orders[$sortableIndex]['column'] = $tempOrderColumns;
}
/**
* performDeferredLeftJoin left joins the deferred bindings table
*/
protected function performDeferredLeftJoin($query = null, $sessionKey = null)
{
$query = $query ?: $this->query;
$query->leftJoin('deferred_bindings', function($join) use ($sessionKey) {
$join->on(
$this->getQualifiedRelatedKeyName(), '=', 'deferred_bindings.slave_id')
->where('master_field', $this->relationName)
->where('master_type', get_class($this->parent))
->where('session_key', $sessionKey);
});
return $this;
}
}
================================================
FILE: src/Database/Relations/DeferOneOrMany.php
================================================
query->getQuery()->newQuery();
$newQuery->from($this->related->getTable());
// Readd the defined constraints
$this->addDefinedConstraintsToQuery($newQuery);
// Apply deferred binding to the new query
$newQuery = $this->withDeferredQuery($newQuery, $sessionKey);
// Bless this query with the deferred query
$this->query->setQuery($newQuery);
// Readd the global scopes
foreach ($this->related->getGlobalScopes() as $identifier => $scope) {
$this->query->withGlobalScope($identifier, $scope);
}
return $this->query;
}
/**
* withDeferredQuery returns the supplied model query, or current model query, with
* deferred bindings added, this will preserve any constraints that came before it
* @param \Illuminate\Database\Query\Builder|null $newQuery
* @param string|null $sessionKey
* @return \Illuminate\Database\Query\Builder
*/
public function withDeferredQuery($newQuery = null, $sessionKey = null)
{
if ($newQuery === null) {
$newQuery = $this->query->getQuery();
}
// Guess the key from the parent model
if ($sessionKey === null) {
$sessionKey = $this->parent->sessionKey;
}
// Swap the standard inner join for a left join
if ($this instanceof BelongsToManyBase) {
$this->performLeftJoin($newQuery);
$this->performSortableColumnJoin($newQuery, $sessionKey);
}
$newQuery->where(function ($query) use ($sessionKey) {
// Trick the relation to add constraints to this nested query
if ($this->parent->exists) {
$oldQuery = $this->query;
$this->query->setQuery($query);
$this->addConstraints();
$this->query->setQuery($oldQuery);
}
// Bind (Add)
$query = $query->orWhereIn($this->related->getQualifiedKeyName(), function ($query) use ($sessionKey) {
$query
->select('slave_id')
->from('deferred_bindings')
->where('master_field', $this->relationName)
->where('master_type', get_class($this->parent))
->where('session_key', $sessionKey)
->where('is_bind', 1);
});
});
// Unbind (Remove)
$newQuery->whereNotIn($this->related->getQualifiedKeyName(), function ($query) use ($sessionKey) {
$query
->select('slave_id')
->from('deferred_bindings')
->where('master_field', $this->relationName)
->where('master_type', get_class($this->parent))
->where('session_key', $sessionKey)
->where('is_bind', 0)
->whereRaw(DbDongle::parse('id > ifnull((select max(id) from '.DbDongle::getTablePrefix().'deferred_bindings where
slave_id = '.$this->getWithDeferredQualifiedKeyName().' and
master_field = ? and
master_type = ? and
session_key = ? and
is_bind = ?
), 0)'), [
$this->relationName,
get_class($this->parent),
$sessionKey,
1
]);
});
return $newQuery;
}
/**
* getWithDeferredQualifiedKeyName returns the related "slave id" key
* in a database friendly format.
* @return \Illuminate\Database\Query\Expression
*/
protected function getWithDeferredQualifiedKeyName()
{
return DbDongle::rawValue(
DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName()
);
}
}
================================================
FILE: src/Database/Relations/DefinedConstraints.php
================================================
'is_published = 1'
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait DefinedConstraints
{
/**
* @var bool pivotSiteScope indicates if pivot queries should be scoped by site_id.
* This is used for dual-multisite scenarios where both parent and related models use multisite.
*/
protected $pivotSiteScope = false;
/**
* addDefinedConstraints to the relation query
*/
public function addDefinedConstraints(): void
{
$args = $this->getRelationDefinitionForDefinedConstraints();
$this->addDefinedConstraintsToRelation($this, $args);
$this->addDefinedConstraintsToQuery($this, $args);
}
/**
* addDefinedConstraintsToRelation
*/
public function addDefinedConstraintsToRelation($relation, ?array $args = null)
{
if ($args === null) {
$args = $this->getRelationDefinitionForDefinedConstraints();
}
// Default models (belongsTo, hasOne, hasOneThrough, morphOne)
if ($defaultData = Arr::get($args, 'default')) {
$relation->withDefault($defaultData);
}
// Pivot data (belongsToMany, morphToMany, morphByMany)
if ($pivotData = Arr::get($args, 'pivot')) {
$relation->withPivot($pivotData);
}
// Pivot incrementing key (belongsToMany, morphToMany, morphByMany)
if ($pivotKey = Arr::get($args, 'pivotKey')) {
$relation->withPivot($pivotKey);
}
// Pivot timestamps (belongsToMany, morphToMany, morphByMany)
if (Arr::get($args, 'timestamps')) {
$relation->withTimestamps();
}
// Count "helper" relation
// @deprecated use Laravel withCount() method instead
if (Arr::get($args, 'count')) {
if ($relation instanceof BelongsToManyBase) {
$relation->countMode = true;
$keyName = $relation->getQualifiedForeignPivotKeyName();
}
else {
$keyName = $relation->getForeignKeyName();
}
$countSql = $this->parent->getConnection()->raw('count(*) as count');
$relation->select($keyName, $countSql)->groupBy($keyName)->orderBy($keyName);
}
// Pivot site scope (dual-multisite: both parent and related models use multisite)
// This enables site_id scoping on pivot table queries
if (Arr::get($args, 'pivotSiteScope')) {
$this->pivotSiteScope = true;
$relation->withPivot(['site_id']);
}
}
/**
* isPivotSiteScoped returns true if pivot queries should be scoped by site_id.
*/
public function isPivotSiteScoped(): bool
{
return $this->pivotSiteScope;
}
/**
* addPivotSiteScopeConstraints adds site_id constraint to pivot queries if enabled.
*/
protected function addPivotSiteScopeConstraints(): void
{
if ($this->pivotSiteScope && !Site::hasGlobalContext()) {
$this->where($this->qualifyPivotColumn('site_id'), Site::getSiteIdFromContext());
}
}
/**
* getPivotSiteScopeValue returns the current site_id value for pivot records.
* Returns null if pivotSiteScope is disabled, otherwise returns the site ID.
*/
protected function getPivotSiteScopeValue(): ?int
{
if (!$this->pivotSiteScope) {
return null;
}
$siteId = Site::getSiteIdFromContext();
// In dual-multisite, we always need a site_id for pivot records
// If there's no site context, it likely means we're in global context
// during propagation - the sync should happen inside Site::withContext()
if ($siteId === null) {
return null;
}
return $siteId;
}
/**
* addDefinedConstraintsToQuery
*/
public function addDefinedConstraintsToQuery($query, ?array $args = null)
{
if ($args === null) {
$args = $this->getRelationDefinitionForDefinedConstraints();
}
// Conditions
if ($conditions = Arr::get($args, 'conditions')) {
$query->whereRaw($conditions);
}
// Sort order
// @deprecated count is deprecated
$hasCountArg = Arr::get($args, 'count') !== null;
if (($orderBy = Arr::get($args, 'order')) && !$hasCountArg) {
if (!is_array($orderBy)) {
$orderBy = [$orderBy];
}
foreach ($orderBy as $order) {
$column = $order;
$direction = 'asc';
$parts = explode(' ', $order);
if (count($parts) > 1) {
[$column, $direction] = $parts;
}
$query->orderBy($column, $direction);
}
}
// Scope
if ($scope = Arr::get($args, 'scope')) {
if (is_string($scope)) {
$query->$scope($this->parent);
}
else {
$scope($query, $this->parent, $this->related);
}
}
}
/**
* getRelationDefinitionForDefinedConstraints returns the relation definition for the
* relationship context.
*/
protected function getRelationDefinitionForDefinedConstraints()
{
return $this->parent->getRelationDefinition($this->relationName);
}
}
================================================
FILE: src/Database/Relations/HasMany.php
================================================
relationName = $relationName;
parent::__construct($query, $parent, $foreignKey, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
$this->parent->unsetRelation($this->relationName);
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function() {
$this->ensureRelationIsEmpty();
});
}
return;
}
if ($value instanceof Model) {
$value = new Collection([$value]);
}
if ($value instanceof CollectionBase) {
$collection = $value;
if ($this->parent->exists) {
$collection->each(function($instance) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
});
}
}
else {
$collection = $this->getRelated()
->whereIn($this->getRelatedKeyName(), (array) $value)
->get()
;
}
if (!$collection) {
return;
}
$this->parent->setRelation($this->relationName, $collection);
$this->parent->bindEventOnce('model.afterSave', function() use ($collection) {
$existingIds = $collection->pluck($this->getRelatedKeyName())->all();
$this->whereNotIn($this->getRelatedKeyName(), $existingIds)->update([
$this->getForeignKeyName() => null
]);
$collection->each(function($instance) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->save(['timestamps' => false]);
});
});
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($this->parent->relationLoaded($relationName)) {
$value = $this->parent->getRelation($relationName)
->pluck($this->getRelatedKeyName())
->all()
;
}
else {
$value = $this->query->getQuery()
->pluck($this->getRelatedKeyName())
->all()
;
}
return $value;
}
}
================================================
FILE: src/Database/Relations/HasManyThrough.php
================================================
relationName = $relationName;
parent::__construct($query, $farParent, $parent, $firstKey, $secondKey, $localKey, $secondLocalKey);
$this->addDefinedConstraints();
}
/**
* getRelationDefinitionForDefinedConstraints returns the relation definition for the
* relationship context.
*/
protected function getRelationDefinitionForDefinedConstraints()
{
return $this->farParent->getRelationDefinition($this->relationName);
}
/**
* parentSoftDeletes determines whether close parent of the relation uses Soft Deletes.
* @return bool
*/
public function parentSoftDeletes()
{
$uses = class_uses_recursive(get_class($this->parent));
return in_array(\October\Rain\Database\Traits\SoftDelete::class, $uses) ||
in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, $uses);
}
/**
* getSimpleValue is a helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($this->farParent->relationLoaded($relationName)) {
$value = $this->farParent->getRelation($relationName)
->pluck($this->getRelatedKeyName())
->all()
;
}
else {
$value = $this->query->getQuery()
->pluck($this->getQualifiedRelatedKeyName())
->all()
;
}
return $value;
}
/**
* getRelatedKeyName
* @return string
*/
public function getRelatedKeyName()
{
return $this->related->getKeyName();
}
/**
* getQualifiedRelatedKeyName
* @return string
*/
public function getQualifiedRelatedKeyName()
{
return $this->related->getQualifiedKeyName();
}
}
================================================
FILE: src/Database/Relations/HasOne.php
================================================
relationName = $relationName;
parent::__construct($query, $parent, $foreignKey, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
if (is_array($value)) {
$value = current($value);
}
// Nulling the relationship
if (!$value) {
$this->parent->setRelation($this->relationName, null);
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function() {
$this->ensureRelationIsEmpty();
});
}
return;
}
if ($value instanceof Model) {
$instance = $value;
if ($this->parent->exists) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
}
}
else {
$instance = $this->getRelated()->find($value);
}
if (!$instance) {
return;
}
$this->parent->setRelation($this->relationName, $instance);
$this->parent->bindEventOnce('model.afterSave', function() use ($instance) {
// Relation is already set, do nothing. This prevents the relationship
// from being nulled below and left unset because the save will ignore
// attribute values that are numerically equivalent (not dirty).
if ($instance->getOriginal($this->getForeignKeyName()) == $this->getParentKey()) {
return;
}
$this->update([$this->getForeignKeyName() => null]);
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->save(['timestamps' => false]);
});
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($related = $this->parent->{$relationName}) {
$key = $this->getRelatedKeyName();
$value = $related->{$key};
}
return $value;
}
}
================================================
FILE: src/Database/Relations/HasOneOrMany.php
================================================
add($model, $sessionKey);
return $model->save() ? $model : false;
}
/**
* saveQuietly saves the supplied related model without raising any events,
* with deferred binding support.
*/
public function saveQuietly(Model $model, $sessionKey = null)
{
return Model::withoutEvents(function () use ($model, $sessionKey) {
return $this->save($model, $sessionKey);
});
}
/**
* saveMany is an alias for the addMany() method
* @param array $models
* @return array
*/
public function saveMany($models, $sessionKey = null)
{
$this->addMany($models, $sessionKey);
return $models;
}
/**
* saveManyQuietly saves multiple models without raising any events,
* with deferred binding support.
*/
public function saveManyQuietly($models, $sessionKey = null)
{
return Model::withoutEvents(function () use ($models, $sessionKey) {
return $this->saveMany($models, $sessionKey);
});
}
/**
* create a new instance of this related model with deferred binding support
*/
public function create(array $attributes = [], $sessionKey = null)
{
$model = parent::create($attributes);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* createQuietly creates a new instance without raising any events,
* with deferred binding support.
*/
public function createQuietly(array $attributes = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $sessionKey) {
return $this->create($attributes, $sessionKey);
});
}
/**
* forceCreateQuietly creates a new instance bypassing mass assignment
* without raising any events, with deferred binding support.
*/
public function forceCreateQuietly(array $attributes = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $sessionKey) {
$model = parent::forceCreate($attributes);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
});
}
/**
* createMany creates multiple related models with deferred binding support.
*/
public function createMany(iterable $records, $sessionKey = null)
{
$instances = parent::createMany($records);
if ($sessionKey !== null) {
foreach ($instances as $model) {
$this->add($model, $sessionKey);
}
}
return $instances;
}
/**
* createManyQuietly creates multiple models without raising any events,
* with deferred binding support.
*/
public function createManyQuietly(iterable $records, $sessionKey = null)
{
return Model::withoutEvents(function () use ($records, $sessionKey) {
return $this->createMany($records, $sessionKey);
});
}
/**
* createOrFirst attempts to create the record, or if a unique constraint
* violation occurs, finds the existing record.
*/
public function createOrFirst(array $attributes = [], \Closure|array $values = [], $sessionKey = null)
{
$model = parent::createOrFirst($attributes, $values);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* add a model to this relationship type
*/
public function add(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
/**
* @event model.relation.beforeAdd
* Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'some_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {
return;
}
// Associate the model
if ($this->parent->exists) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->save();
}
else {
$this->parent->bindEventOnce('model.afterSave', function () use ($model) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->save();
});
}
// Use the opportunity to set the relation in memory
if ($this instanceof HasOne) {
$this->parent->setRelation($this->relationName, $model);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.add
* Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.add', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);
}
else {
$this->parent->bindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* addMany attaches an array of models to the parent instance with deferred binding support
* @param array $models
* @return void
*/
public function addMany($models, $sessionKey = null)
{
foreach ($models as $model) {
$this->add($model, $sessionKey);
}
}
/**
* remove a model from this relationship type.
*/
public function remove(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
/**
* @event model.relation.beforeRemove
* Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'perm_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {
return;
}
if (!$this->isModelRemovable($model)) {
return;
}
$options = $this->parent->getRelationDefinition($this->relationName);
// Delete or orphan the model
if (Arr::get($options, 'delete', false)) {
$model->delete();
}
else {
$model->setAttribute($this->getForeignKeyName(), null);
$model->save();
}
// Use this opportunity to set the relation in memory
if ($this instanceof HasOne) {
$this->parent->setRelation($this->relationName, null);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.remove
* Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.remove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was removed from {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);
}
else {
$this->parent->unbindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* isModelRemovable returns true if an existing model is already associated
*/
protected function isModelRemovable($model): bool
{
return ((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey());
}
/**
* ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.
*/
protected function ensureRelationIsEmpty()
{
$options = $this->parent->getRelationDefinition($this->relationName);
if (Arr::get($options, 'delete', false)) {
$this->delete();
}
else {
$this->update([$this->getForeignKeyName() => null]);
}
}
/**
* getRelatedKeyName
* @return string
*/
public function getRelatedKeyName()
{
return $this->related->getKeyName();
}
/**
* @deprecated use getForeignKeyName
*/
public function getForeignKey()
{
return $this->foreignKey;
}
/**
* @deprecated use getLocalKeyName
*/
public function getOtherKey()
{
return $this->localKey;
}
}
================================================
FILE: src/Database/Relations/HasOneThrough.php
================================================
relationName = $relationName;
parent::__construct($query, $farParent, $parent, $firstKey, $secondKey, $localKey, $secondLocalKey);
$this->addDefinedConstraints();
}
/**
* parentSoftDeletes determines whether close parent of the relation uses Soft Deletes.
* @return bool
*/
public function parentSoftDeletes()
{
$uses = class_uses_recursive(get_class($this->parent));
return in_array(\October\Rain\Database\Traits\SoftDelete::class, $uses) ||
in_array(\Illuminate\Database\Eloquent\SoftDeletes::class, $uses);
}
}
================================================
FILE: src/Database/Relations/MorphMany.php
================================================
relationName = $relationName;
parent::__construct($query, $parent, $type, $id, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function () {
$this->ensureRelationIsEmpty();
});
}
return;
}
if ($value instanceof Model) {
$value = new Collection([$value]);
}
if ($value instanceof CollectionBase) {
$collection = $value;
if ($this->parent->exists) {
$collection->each(function ($instance) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->setAttribute($this->getMorphType(), $this->morphClass);
});
}
}
else {
$collection = $this->getRelated()
->whereIn($this->getRelatedKeyName(), (array) $value)
->get()
;
}
if ($collection) {
$this->parent->setRelation($this->relationName, $collection);
$this->parent->bindEventOnce('model.afterSave', function () use ($collection) {
$existingIds = $collection->pluck($this->getRelatedKeyName())->all();
$this->whereNotIn($this->getRelatedKeyName(), $existingIds)->update([
$this->getForeignKeyName() => null,
$this->getMorphType() => null
]);
$collection->each(function ($instance) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->setAttribute($this->getMorphType(), $this->morphClass);
$instance->save(['timestamps' => false]);
});
});
}
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($this->parent->relationLoaded($relationName)) {
$value = $this->parent->getRelation($relationName)
->pluck($this->getRelatedKeyName())
->all()
;
}
else {
$value = $this->query->getQuery()
->pluck($this->getRelatedKeyName())
->all()
;
}
return $value;
}
}
================================================
FILE: src/Database/Relations/MorphOne.php
================================================
relationName = $relationName;
parent::__construct($query, $parent, $type, $id, $localKey);
$this->addDefinedConstraints();
}
/**
* setSimpleValue helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
if (is_array($value)) {
$value = current($value);
}
// Nulling the relationship
if (!$value) {
if ($this->parent->exists) {
$this->parent->bindEventOnce('model.afterSave', function() {
$this->ensureRelationIsEmpty();
});
}
return;
}
if ($value instanceof Model) {
$instance = $value;
if ($this->parent->exists) {
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->setAttribute($this->getMorphType(), $this->morphClass);
}
}
else {
$instance = $this->getRelated()->find($value);
}
if ($instance) {
$this->parent->setRelation($this->relationName, $instance);
$this->parent->bindEventOnce('model.afterSave', function () use ($instance) {
// Relation is already set, do nothing. This prevents the relationship
// from being nulled below and left unset because the save will ignore
// attribute values that are numerically equivalent (not dirty).
if (
$instance->getOriginal($this->getForeignKeyName()) == $this->getParentKey() &&
$instance->getOriginal($this->getMorphType()) == $this->morphClass
) {
return;
}
$this->update([
$this->getForeignKeyName() => null,
$this->getMorphType() => null
]);
$instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$instance->setAttribute($this->getMorphType(), $this->morphClass);
$instance->save(['timestamps' => false]);
});
}
}
/**
* getSimpleValue helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
$value = null;
$relationName = $this->relationName;
if ($related = $this->parent->$relationName) {
$key = $this->getRelatedKeyName();
$value = $related->{$key};
}
return $value;
}
}
================================================
FILE: src/Database/Relations/MorphOneOrMany.php
================================================
add($model, $sessionKey);
return $model->save() ? $model : false;
}
/**
* saveQuietly saves the supplied related model without raising any events,
* with deferred binding support.
*/
public function saveQuietly(Model $model, $sessionKey = null)
{
return Model::withoutEvents(function () use ($model, $sessionKey) {
return $this->save($model, $sessionKey);
});
}
/**
* saveMany saves multiple models with deferred binding support.
*/
public function saveMany($models, $sessionKey = null)
{
foreach ($models as $model) {
$this->save($model, $sessionKey);
}
return $models;
}
/**
* saveManyQuietly saves multiple models without raising any events,
* with deferred binding support.
*/
public function saveManyQuietly($models, $sessionKey = null)
{
return Model::withoutEvents(function () use ($models, $sessionKey) {
return $this->saveMany($models, $sessionKey);
});
}
/**
* create a new instance of this related model with deferred binding support.
*/
public function create(array $attributes = [], $sessionKey = null)
{
$model = parent::create($attributes);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* createQuietly creates a new instance without raising any events,
* with deferred binding support.
*/
public function createQuietly(array $attributes = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $sessionKey) {
return $this->create($attributes, $sessionKey);
});
}
/**
* forceCreateQuietly creates a new instance bypassing mass assignment
* without raising any events, with deferred binding support.
*/
public function forceCreateQuietly(array $attributes = [], $sessionKey = null)
{
return Model::withoutEvents(function () use ($attributes, $sessionKey) {
$model = parent::forceCreate($attributes);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
});
}
/**
* createMany creates multiple related models with deferred binding support.
*/
public function createMany(iterable $records, $sessionKey = null)
{
$instances = parent::createMany($records);
if ($sessionKey !== null) {
foreach ($instances as $model) {
$this->add($model, $sessionKey);
}
}
return $instances;
}
/**
* createManyQuietly creates multiple models without raising any events,
* with deferred binding support.
*/
public function createManyQuietly(iterable $records, $sessionKey = null)
{
return Model::withoutEvents(function () use ($records, $sessionKey) {
return $this->createMany($records, $sessionKey);
});
}
/**
* createOrFirst attempts to create the record, or if a unique constraint
* violation occurs, finds the existing record.
*/
public function createOrFirst(array $attributes = [], \Closure|array $values = [], $sessionKey = null)
{
$model = parent::createOrFirst($attributes, $values);
if ($sessionKey !== null) {
$this->add($model, $sessionKey);
}
return $model;
}
/**
* add a model to this relationship type.
*/
public function add(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
/**
* @event model.relation.beforeAdd
* Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'some_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {
return;
}
// Associate the model
if ($this->parent->exists) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->setAttribute($this->getMorphType(), $this->morphClass);
$model->save();
}
else {
$this->parent->bindEventOnce('model.afterSave', function () use ($model) {
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
$model->setAttribute($this->getMorphType(), $this->morphClass);
$model->save();
});
}
// Use the opportunity to set the relation in memory
if ($this instanceof MorphOne) {
$this->parent->setRelation($this->relationName, $model);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.add
* Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.add', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was added as {$relationName} to {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);
}
else {
$this->parent->bindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* remove a model from this relationship type.
*/
public function remove(Model $model, $sessionKey = null)
{
if ($sessionKey === null) {
/**
* @event model.relation.beforeRemove
* Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'perm_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {
return;
}
if (!$this->isModelRemovable($model)) {
return;
}
$options = $this->parent->getRelationDefinition($this->relationName);
// Delete or orphan the model
if (Arr::get($options, 'delete', false)) {
$model->delete();
}
else {
$model->setAttribute($this->getForeignKeyName(), null);
$model->setAttribute($this->getMorphType(), null);
$model->save();
}
// Use this opportunity to set the relation in memory
if ($this instanceof MorphOne) {
$this->parent->setRelation($this->relationName, null);
}
else {
$this->parent->unsetRelation($this->relationName);
}
/**
* @event model.relation.remove
* Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.remove', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was removed from {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);
}
else {
$this->parent->unbindDeferred($this->relationName, $model, $sessionKey);
}
}
/**
* isModelRemovable returns true if an existing model is already associated
*/
protected function isModelRemovable($model): bool
{
return
((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey()) &&
$model->getAttribute($this->getMorphType()) === $this->morphClass;
}
/**
* ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.
*/
protected function ensureRelationIsEmpty()
{
$options = $this->parent->getRelationDefinition($this->relationName);
if (Arr::get($options, 'delete', false)) {
$this->delete();
}
else {
$this->update([
$this->getForeignKeyName() => null,
$this->getMorphType() => null
]);
}
}
/**
* getRelatedKeyName
* @return string
*/
public function getRelatedKeyName()
{
return $this->related->getKeyName();
}
}
================================================
FILE: src/Database/Relations/MorphTo.php
================================================
relationName = $relationName;
parent::__construct($query, $parent, $foreignKey, $otherKey, $type, $relationName);
$this->addDefinedConstraints();
}
/**
* associate the model instance to the given parent.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
public function associate($model)
{
/**
* @event model.relation.beforeAssociate
* Called before associating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeAssociate', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'some_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeAssociate', [$this->relationName, $model], true) === false) {
return;
}
$result = parent::associate($model);
/**
* @event model.relation.associate
* Called after associating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.associate', function (string $relationName, \October\Rain\Database\Model $relatedModel) use (\October\Rain\Database\Model $model) {
* $relatedClass = get_class($relatedModel);
* $modelClass = get_class($model);
* traceLog("{$relatedClass} was associated as {$relationName} to {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.associate', [$this->relationName, $model]);
return $result;
}
/**
* dissociate previously dissociated model from the given parent.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function dissociate()
{
/**
* @event model.relation.beforeDissociate
* Called before dissociating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\October\Rain\Database\Model $model) {
* if ($relationName === 'perm_relation') {
* return false;
* }
* });
*
*/
if ($this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName], true) === false) {
return;
}
$result = parent::dissociate();
/**
* @event model.relation.dissociate
* Called after dissociating a relation to the model (only for BelongsTo/MorphTo relations)
*
* Example usage:
*
* $model->bindEvent('model.relation.dissociate', function (string $relationName) use (\October\Rain\Database\Model $model) {
* $modelClass = get_class($model);
* traceLog("{$relationName} was dissociated from {$modelClass}.");
* });
*
*/
$this->parent->fireEvent('model.relation.dissociate', [$this->relationName]);
return $result;
}
/**
* setSimpleValue is a helper for setting this relationship using various expected
* values. For example, $model->relation = $value;
*/
public function setSimpleValue($value)
{
// Nulling the relationship
if (!$value) {
$this->dissociate();
return;
}
if ($value instanceof Model) {
// Non existent model, use a single serve event to associate it again when ready
if (!$value->exists) {
$value->bindEventOnce('model.afterSave', function () use ($value) {
$this->associate($value);
});
}
$this->associate($value);
$this->parent->setRelation($this->relationName, $value);
}
elseif (is_array($value)) {
[$modelId, $modelClass] = $value;
$this->parent->setAttribute($this->foreignKey, $modelId);
$this->parent->setAttribute($this->morphType, $modelClass);
$this->parent->unsetRelation($this->relationName);
}
else {
$this->parent->setAttribute($this->foreignKey, $value);
$this->parent->unsetRelation($this->relationName);
}
}
/**
* getSimpleValue is a helper for getting this relationship simple value,
* generally useful with form values.
*/
public function getSimpleValue()
{
return [
$this->parent->getAttribute($this->foreignKey),
$this->parent->getAttribute($this->morphType)
];
}
}
================================================
FILE: src/Database/Relations/MorphToMany.php
================================================
inverse = $inverse;
$this->morphType = $name.'_type';
$this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass();
parent::__construct(
$query,
$parent,
$table,
$foreignKey,
$otherKey,
$parentKey,
$relatedKey,
$relationName
);
$this->addDefinedConstraints();
}
/**
* addWhereConstraints set the where clause for the relation query
* @return $this
*/
protected function addWhereConstraints()
{
parent::addWhereConstraints();
$this->query->where($this->table.'.'.$this->morphType, $this->morphClass);
return $this;
}
/**
* addEagerConstraints sets the constraints for an eager load of the relation
* @param array $models
* @return void
*/
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
$this->query->where($this->table.'.'.$this->morphType, $this->morphClass);
}
/**
* baseAttachRecord creates a new pivot attachment record.
* @param int $id
* @param bool $timed
* @return array
*/
protected function baseAttachRecord($id, $timed)
{
return Arr::add(
parent::baseAttachRecord($id, $timed),
$this->morphType,
$this->morphClass
);
}
/**
* getRelationExistenceQuery adds the constraints for a relationship count query.
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(
$this->table.'.'.$this->morphType,
$this->morphClass
);
}
/**
* newPivotQuery creates a new query builder for the pivot table.
*/
public function newPivotQuery()
{
return parent::newPivotQuery()->where($this->morphType, $this->morphClass);
}
/**
* newPivot creates a new pivot model instance
* @param array $attributes
* @param bool $exists
* @return \Illuminate\Database\Eloquent\Relations\Pivot
*/
public function newPivot(array $attributes = [], $exists = false)
{
// October looks to the relationship parent
$pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists);
// Laravel creates new pivot model this way
if (empty($pivot)) {
$using = $this->using;
$pivot = $using
? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists)
: MorphPivot::fromAttributes($this->parent, $attributes, $this->table, $exists);
}
$pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)
->setMorphType($this->morphType)
->setMorphClass($this->morphClass);
return $pivot;
}
/**
* getMorphType gets the foreign key "type" name
*/
public function getMorphType()
{
return $this->morphType;
}
/**
* getMorphClass get the class name of the parent model
*/
public function getMorphClass()
{
return $this->morphClass;
}
}
================================================
FILE: src/Database/Relations/Relation.php
================================================
'App\Post',
* 'videos' => 'App\Video',
* ]);
*
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
abstract class Relation extends RelationBase
{
}
================================================
FILE: src/Database/Replicator.php
================================================
model = $model;
$this->isMultisite = $model->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class);
}
/**
* replicate replicates the model into a new, non-existing instance,
* including replicating relations.
*
* @param array|null $except
* @return static
*/
public function replicate(?array $except = null)
{
$this->isDuplicating = false;
return $this->replicateRelationsInternal($except);
}
/**
* duplicate replicates a model with special multisite duplication logic.
* To avoid duplication of has many relations, the logic only propagates relations on
* the parent model since they are shared via site_root_id beyond this point.
*
* @param array|null $except
* @return static
*/
public function duplicate(?array $except = null)
{
$this->isDuplicating = true;
return $this->replicateRelationsInternal($except);
}
/**
* replicateRelationsInternal
*/
protected function replicateRelationsInternal(?array $except = null)
{
$defaults = [
$this->model->getKeyName(),
$this->model->getCreatedAtColumn(),
$this->model->getUpdatedAtColumn(),
];
if ($this->isMultisite) {
$defaults[] = 'site_root_id';
}
$attributes = Arr::except(
$this->model->attributes,
$except ? array_unique(array_merge($except, $defaults)) : $defaults
);
$instance = $this->model->newReplicationInstance($attributes);
$definitions = $this->model->getRelationDefinitions();
foreach ($definitions as $type => $relations) {
foreach ($relations as $name => $options) {
if ($this->isRelationReplicable($name)) {
$this->replicateRelationInternal($instance->$name(), $this->model->$name);
}
}
}
return $instance;
}
/**
* replicateRelationInternal on the model instance with the supplied ones
*/
protected function replicateRelationInternal($relationObject, $models)
{
if ($models instanceof CollectionBase) {
$models = $models->all();
}
elseif ($models instanceof EloquentModel) {
$models = [$models];
}
else {
$models = (array) $models;
}
$this->associationMap = [];
foreach (array_filter($models) as $model) {
if ($relationObject instanceof HasOneOrMany) {
$relationObject->add($newModel = $model->replicateWithRelations());
$this->mapAssociation($model, $newModel);
}
else {
$relationObject->add($model);
}
}
$relatedModel = $relationObject->getRelated();
if ($relatedModel->isClassInstanceOf(\October\Contracts\Database\TreeInterface::class)) {
$this->updateTreeAssociations();
}
}
/**
* isRelationReplicable determines whether the specified relation should be replicated
* when replicateWithRelations() is called instead of save() on the model. Default: true.
*/
protected function isRelationReplicable(string $name): bool
{
// Relation is shared via propagation
if (
!$this->isDuplicating &&
$this->isMultisite &&
$this->model->isAttributePropagatable($name)
) {
return false;
}
return $this->model->isRelationReplicable($name);
}
/**
* mapAssociation is an internal method that keeps a record of what records were created
* and their associated source, the following format is used:
*
* [FromModel::id] => [FromModel, ToModel]
*/
protected function mapAssociation($currentModel, $replicatedModel)
{
$this->associationMap[$currentModel->getKey()] = [$currentModel, $replicatedModel];
}
/**
* updateTreeAssociations sets new parents on the replicated records
*/
protected function updateTreeAssociations()
{
foreach ($this->associationMap as $tuple) {
[$currentModel, $replicatedModel] = $tuple;
$newParent = $this->associationMap[$currentModel->getParentId()][1] ?? null;
$replicatedModel->parent = $newParent;
}
}
}
================================================
FILE: src/Database/Schema/Blueprint.php
================================================
unsignedBigInteger($column)->nullable();
$this->unsignedBigInteger('site_root_id')->nullable();
$this->index([$column, 'site_root_id'], $indexName);
}
}
================================================
FILE: src/Database/Scopes/MultisiteGroupScope.php
================================================
isMultisiteGroupEnabled() && !Site::hasGlobalContext()) {
$builder->where(
$model->getQualifiedSiteGroupIdColumn(),
Site::getSiteGroupIdFromContext()
);
}
}
/**
* extend the Eloquent query builder.
*/
public function extend(BuilderBase $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
}
/**
* addWithSiteGroup removes this scope and includes the specified site group
*/
protected function addWithSiteGroup(BuilderBase $builder)
{
$builder->macro('withSiteGroup', function (BuilderBase $builder, $groupId) {
return $builder
->withoutGlobalScope($this)
->where($builder->getModel()->getQualifiedSiteGroupIdColumn(), $groupId)
;
});
}
/**
* addWithSiteGroups removes this scope and includes everything,
* or filters by an array of group ids.
*/
protected function addWithSiteGroups(BuilderBase $builder)
{
$builder->macro('withSiteGroups', function (BuilderBase $builder, $groupIds = null) {
if (!is_array($groupIds)) {
return $builder->withoutGlobalScope($this);
}
return $builder
->withoutGlobalScope($this)
->whereIn($builder->getModel()->getQualifiedSiteGroupIdColumn(), $groupIds)
;
});
}
}
================================================
FILE: src/Database/Scopes/MultisiteScope.php
================================================
isMultisiteEnabled() && !Site::hasGlobalContext()) {
$builder->where($model->getQualifiedSiteIdColumn(), Site::getSiteIdFromContext());
}
}
/**
* extend the Eloquent query builder.
*/
public function extend(BuilderBase $builder)
{
foreach ($this->extensions as $extension) {
$this->{"add{$extension}"}($builder);
}
}
/**
* addWithSite removes this scope and includes the specified site
*/
protected function addWithSite(BuilderBase $builder)
{
$builder->macro('withSite', function (BuilderBase $builder, $siteId) {
return $builder
->withoutGlobalScope($this)
->where($builder->getModel()->getQualifiedSiteIdColumn(), $siteId)
;
});
}
/**
* addWithSites removes this scope and includes everything.
*/
protected function addWithSites(BuilderBase $builder)
{
$builder->macro('withSites', function (BuilderBase $builder, $siteIds = null) {
if (!is_array($siteIds)) {
return $builder->withoutGlobalScope($this);
}
return $builder
->withoutGlobalScope($this)
->whereIn($builder->getModel()->getQualifiedSiteIdColumn(), $siteIds)
;
});
}
/**
* addWithSyncSites removes this scope and includes sites that should be synced with this model
*/
protected function addWithSyncSites(BuilderBase $builder)
{
$builder->macro('withSyncSites', function (BuilderBase $builder) {
return $builder->withSites($builder->getModel()->getMultisiteSyncSites());
});
}
}
================================================
FILE: src/Database/Scopes/NestedTreeScope.php
================================================
getQuery()->orderBy($model->getLeftColumnName());
}
/**
* extend the Eloquent query builder.
*/
public function extend(BuilderBase $builder)
{
$removeOnMethods = ['reorder', 'orderBy', 'groupBy'];
foreach ($removeOnMethods as $method) {
$builder->macro($method, function ($builder, ...$args) use ($method) {
$builder
->withoutGlobalScope($this)
->getQuery()
->$method(...$args)
;
return $builder;
});
}
}
}
================================================
FILE: src/Database/Scopes/SoftDeleteScope.php
================================================
isSoftDeleteEnabled()) {
$builder->whereNull($model->getQualifiedDeletedAtColumn());
}
}
}
================================================
FILE: src/Database/Scopes/SortableScope.php
================================================
getQuery()->orderBy($model->getQualifiedSortOrderColumn());
}
/**
* extend the Eloquent query builder.
*/
public function extend(BuilderBase $builder)
{
$removeOnMethods = ['reorder', 'orderBy', 'groupBy'];
foreach ($removeOnMethods as $method) {
$builder->macro($method, function ($builder, ...$args) use ($method) {
$builder
->withoutGlobalScope($this)
->getQuery()
->$method(...$args)
;
return $builder;
});
}
}
}
================================================
FILE: src/Database/SortableScope.php
================================================
string('baseid')->nullable()->index();
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait BaseIdentifier
{
/**
* initializeBaseIdentifier trait for a model.
*/
public function initializeBaseIdentifier()
{
$this->bindEvent('model.saveInternal', function () {
$this->baseIdentifyAttributes();
});
}
/**
* baseIdentifyAttributes
*/
public function baseIdentifyAttributes()
{
$baseidAttribute = $this->getBaseIdentifierColumnName();
if (!$this->{$baseidAttribute}) {
$this->attributes[$baseidAttribute] = $this->getBaseIdentifierUniqueAttributeValue($baseidAttribute);
}
}
/**
* generateBaseIdentifier returns a random encoded 64 bit number
*/
public function generateBaseIdentifier()
{
return rtrim(strtr(base64_encode(random_bytes(8)), '+/', '-_'), '=');
}
/**
* getBaseIdentifierUniqueAttributeValue ensures a unique attribute value, if the value
* is already used another base identifier is created. Returns a safe value that is unique.
* @param string $name
* @return string
*/
protected function getBaseIdentifierUniqueAttributeValue($name)
{
$value = $this->generateBaseIdentifier();
while ($this->newQueryWithoutScopes()->where($name, $value)->count() > 0) {
$value = $this->generateBaseIdentifier();
}
return $value;
}
/**
* getBaseIdentifierColumnName gets the name of the "baseid" column.
* @return string
*/
public function getBaseIdentifierColumnName()
{
return defined('static::BASEID') ? static::BASEID : 'baseid';
}
}
================================================
FILE: src/Database/Traits/Defaultable.php
================================================
bindEvent('model.afterSave', [$this, 'defaultableAfterSave']);
}
/**
* defaultableAfterSave
*/
public function defaultableAfterSave()
{
if ($this->is_default) {
$this->makeDefault();
}
}
/**
* makeDefault
*/
public function makeDefault()
{
$this->newQuery()->where('id', $this->id)->update(['is_default' => true]);
$this->newQuery()->where('id', '<>', $this->id)->update(['is_default' => false]);
}
/**
* clearDefaultableCache clears the default record cache
*/
public static function clearDefaultableCache()
{
static::$defaultableCache = null;
}
/**
* getDefault returns the default product type.
*/
public static function getDefault()
{
if (static::$defaultableCache !== null) {
return static::$defaultableCache;
}
$defaultType = static::where('is_default', true)->first();
// If no default is found, find the first record and make it the default.
if (!$defaultType && ($defaultType = static::first())) {
$defaultType->makeDefault();
}
return static::$defaultableCache = $defaultType;
}
}
================================================
FILE: src/Database/Traits/DeferredBinding.php
================================================
hasRelation($relationName)) {
return false;
}
return in_array(
$this->getRelationType($relationName),
$this->getDeferrableRelationTypes()
);
}
/**
* hasDeferred returns true if a deferred record exists for a relation
*/
public function hasDeferred($sessionKey = null, $relationName = null): bool
{
if ($sessionKey === null) {
$sessionKey = $this->sessionKey;
}
return DeferredBindingModel::hasDeferredActions(get_class($this), $sessionKey, $relationName);
}
/**
* bindDeferred binds a deferred relationship to the supplied record
*/
public function bindDeferred($relation, $record, $sessionKey, $pivotData = []): DeferredBindingModel
{
$binding = new DeferredBindingModel;
$binding->setConnection($this->getConnectionName());
$binding->master_type = get_class($this);
$binding->master_field = $relation;
$binding->slave_type = get_class($record);
$binding->slave_id = $record->getKey();
$binding->pivot_data = $pivotData;
$binding->session_key = $sessionKey;
$binding->is_bind = true;
/**
* @event deferredBinding.newBindInstance
* Called after the DeferredBindingModel is initialized for binding
*
* Example usage:
*
* $model->bindEvent('deferredBinding.newBindInstance', function ((\Model) $model) {
* $model->some_attribute = true;
* });
*
*/
if ($event = $this->fireEvent('deferredBinding.newBindInstance', $binding, true)) {
$binding = $event;
}
$binding->save();
return $binding;
}
/**
* unbindDeferred unbinds a deferred relationship to the supplied record
*/
public function unbindDeferred($relation, $record, $sessionKey): DeferredBindingModel
{
$binding = new DeferredBindingModel;
$binding->setConnection($this->getConnectionName());
$binding->master_type = get_class($this);
$binding->master_field = $relation;
$binding->slave_type = get_class($record);
$binding->slave_id = $record->getKey();
$binding->session_key = $sessionKey;
$binding->is_bind = false;
/**
* @event deferredBinding.newUnbindInstance
* Called after the DeferredBindingModel is initialized for unbinding
*
* Example usage:
*
* $model->bindEvent('deferredBinding.newUnbindInstance', function ((\Model) $model) {
* $model->some_attribute = true;
* });
*
*/
if ($event = $this->fireEvent('deferredBinding.newUnbindInstance', $binding, true)) {
$binding = $event;
}
$binding->save();
return $binding;
}
/**
* cancelDeferred cancels all deferred bindings to this model
*/
public function cancelDeferred($sessionKey): void
{
DeferredBindingModel::cancelDeferredActions(get_class($this), $sessionKey);
}
/**
* commitDeferred commits all deferred bindings to this model
*/
public function commitDeferred($sessionKey)
{
$this->commitDeferredOfType($sessionKey);
}
/**
* commitDeferredBefore is used internally to commit all deferred bindings before saving.
* It is a rare need to have to call this, since it only applies to the
* "belongs to" relationship which generally does not need deferring.
*/
protected function commitDeferredBefore($sessionKey)
{
$this->commitDeferredOfType($sessionKey, 'belongsTo');
}
/**
* commitDeferredAfter is used internally to commit all deferred bindings after saving
*/
protected function commitDeferredAfter($sessionKey)
{
$this->commitDeferredOfType($sessionKey, null, 'belongsTo');
}
/**
* commitDeferredOfType is an internal method for committing deferred relations
*/
protected function commitDeferredOfType($sessionKey, $include = null, $exclude = null)
{
if (!strlen($sessionKey)) {
return;
}
$bindings = $this->getDeferredBindingRecords($sessionKey);
foreach ($bindings as $binding) {
if (!($relationName = $binding->master_field)) {
continue;
}
if (!$this->hasRelation($relationName)) {
continue;
}
$relationType = $this->getRelationType($relationName);
$allowedTypes = $this->getDeferrableRelationTypes();
if ($include) {
$allowedTypes = array_intersect($allowedTypes, (array) $include);
}
elseif ($exclude) {
$allowedTypes = array_diff($allowedTypes, (array) $exclude);
}
if (!in_array($relationType, $allowedTypes)) {
continue;
}
// Find the slave model
$slaveClass = $binding->slave_type;
$slaveModel = $this->makeRelation($relationName);
if (!is_a($slaveModel, $slaveClass)) {
continue;
}
$slaveModel = $slaveModel->find($binding->slave_id);
if (!$slaveModel) {
continue;
}
// Bind/Unbind the relationship, save the related model with any
// deferred bindings it might have and delete the binding action
$relationObj = $this->$relationName();
if ($binding->is_bind) {
if (in_array($relationType, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {
$pivotData = $binding->getPivotDataForBind($this, $relationName);
$relationObj->add($slaveModel, null, $pivotData);
}
else {
$relationObj->add($slaveModel);
}
}
else {
$relationObj->remove($slaveModel);
}
$binding->delete();
}
}
/**
* getDeferredBindingRecords returns any outstanding binding records for this model
* @return \October\Rain\Database\Collection
*/
protected function getDeferredBindingRecords($sessionKey)
{
$binding = new DeferredBindingModel;
$binding->setConnection($this->getConnectionName());
return $binding
->where('master_type', get_class($this))
->where('session_key', $sessionKey)
->get()
;
}
/**
* getDeferrableRelationTypes returns all possible relation types that can be deferred
* @return array
*/
protected function getDeferrableRelationTypes()
{
return [
'hasMany',
'hasOne',
'morphMany',
'morphToMany',
'morphedByMany',
'morphOne',
'attachMany',
'attachOne',
'belongsToMany',
'belongsTo'
];
}
}
================================================
FILE: src/Database/Traits/Encryptable.php
================================================
encryptable)) {
throw new Exception(sprintf(
'The $encryptable property in %s must be an array to use the Encryptable trait.',
static::class
));
}
// Encrypt required fields when necessary
$this->bindEvent('model.beforeSetAttribute', function ($key, $value) {
if (
in_array($key, $this->getEncryptableAttributes()) &&
$value !== null &&
$value !== ''
) {
return $this->makeEncryptableValue($key, $value);
}
});
$this->bindEvent('model.beforeGetAttribute', function ($key) {
if (
in_array($key, $this->getEncryptableAttributes()) &&
Arr::get($this->attributes, $key) !== null &&
Arr::get($this->attributes, $key) !== ''
) {
return $this->getEncryptableValue($key);
}
});
}
/**
* makeEncryptableValue encrypts an attribute value and saves it in the original locker
* @param string $key Attribute
* @param string $value Value to encrypt
* @return string Encrypted value
*/
public function makeEncryptableValue($key, $value)
{
$this->originalEncryptableValues[$key] = $value;
return Crypt::encrypt($value);
}
/**
* getEncryptableValue decrypts an attribute value
* @param string $key Attribute
* @return string Decrypted value
*/
public function getEncryptableValue($key)
{
return Crypt::decrypt($this->attributes[$key]);
}
/**
* getEncryptableAttributes returns a collection of fields that will be encrypted.
* @return array
*/
public function getEncryptableAttributes()
{
return $this->encryptable;
}
/**
* getOriginalEncryptableValues returns the original values of any encrypted attributes
* @return array
*/
public function getOriginalEncryptableValues()
{
return $this->originalEncryptableValues;
}
/**
* getOriginalEncryptableValue returns the original values of any encrypted attributes.
* @return mixed
*/
public function getOriginalEncryptableValue($attribute)
{
return $this->originalEncryptableValues[$attribute] ?? null;
}
}
================================================
FILE: src/Database/Traits/Hashable.php
================================================
hashable)) {
throw new Exception(sprintf(
'The $hashable property in %s must be an array to use the Hashable trait.',
static::class
));
}
// Hash required fields when necessary
$this->bindEvent('model.beforeSetAttribute', function ($key, $value) {
$hashable = $this->getHashableAttributes();
if (in_array($key, $hashable) && !empty($value)) {
return $this->makeHashValue($key, $value);
}
});
}
/**
* addHashable adds an attribute to the hashable attributes list
* @param array|string|null $attributes
* @return $this
*/
public function addHashable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->hashable = array_merge($this->hashable, $attributes);
return $this;
}
/**
* makeHashValue hashes an attribute value and saves it in the original locker.
* @param string $key Attribute
* @param string $value Value to hash
* @return string Hashed value
*/
public function makeHashValue($key, $value)
{
$this->originalHashableValues[$key] = $value;
return Hash::make($value);
}
/**
* checkHashValue checks if the supplied plain value matches the stored hash value.
* @param string $key Attribute to check
* @param string $value Value to check
* @return bool
*/
public function checkHashValue($key, $value)
{
return Hash::check($value, $this->{$key});
}
/**
* getHashableAttributes returns a collection of fields that will be hashed.
* @return array
*/
public function getHashableAttributes()
{
return $this->hashable;
}
/**
* getOriginalHashValues returns the original values of any hashed attributes.
* @return array
*/
public function getOriginalHashValues()
{
return $this->originalHashableValues;
}
/**
* getOriginalHashValue returns the original values of any hashed attributes.
* @return mixed
*/
public function getOriginalHashValue($attribute)
{
return $this->originalHashableValues[$attribute] ?? null;
}
}
================================================
FILE: src/Database/Traits/Multisite.php
================================================
propagatable)) {
throw new Exception(sprintf(
'The $propagatable property in %s must be an array to use the Multisite trait.',
static::class
));
}
$this->bindEvent('model.beforeSave', [$this, 'multisiteBeforeSave']);
$this->bindEvent('model.afterCreate', [$this, 'multisiteAfterCreate']);
$this->bindEvent('model.saveComplete', [$this, 'multisiteSaveComplete']);
$this->bindEvent('model.afterDelete', [$this, 'multisiteAfterDelete']);
if (!$this->multisiteRelationsDefined) {
$this->defineMultisiteRelations();
$this->multisiteRelationsDefined = true;
}
}
/**
* multisiteBeforeSave constructor event used internally
*/
public function multisiteBeforeSave()
{
if (Site::hasGlobalContext()) {
return;
}
$this->{$this->getSiteIdColumn()} = Site::getSiteIdFromContext();
}
/**
* multisiteSaveComplete constructor event used internally
*/
public function multisiteSaveComplete()
{
if ($this->getSaveOption('propagate') !== true) {
return;
}
if (!$this->isMultisiteEnabled()) {
return;
}
Site::withGlobalContext(function() {
$otherModels = $this->newOtherSiteQuery()->get();
$otherSites = $this->getMultisiteSyncSites();
// Propagate attributes to known records
if ($this->propagatable) {
foreach ($otherSites as $siteId) {
if ($model = $otherModels->where('site_id', $siteId)->first()) {
$this->propagateToSite($siteId, $model);
}
}
}
// Sync non-existent records
if ($this->isMultisiteSyncEnabled()) {
$missingSites = array_diff($otherSites, $otherModels->pluck('site_id')->all());
foreach ($missingSites as $missingSite) {
$this->propagateToSite($missingSite);
}
}
});
}
/**
* multisiteAfterCreate constructor event used internally
*/
public function multisiteAfterCreate()
{
if ($this->site_root_id) {
return;
}
$this->site_root_id = $this->id;
$this->newQueryWithoutScopes()
->where($this->getKeyName(), $this->id)
->update(['site_root_id' => $this->site_root_id])
;
}
/**
* multisiteAfterDelete
*/
public function multisiteAfterDelete()
{
if (!$this->isMultisiteSyncEnabled() || !$this->getMultisiteConfig('delete', true)) {
return;
}
Site::withGlobalContext(function() {
foreach ($this->getMultisiteSyncSites() as $siteId) {
if (!$this->isModelUsingSameSite($siteId)) {
$this->deleteForSite($siteId);
}
}
});
}
/**
* defineMultisiteRelations will spin over every relation and apply propagation config
*/
protected function defineMultisiteRelations()
{
foreach ($this->getRelationDefinitions() as $type => $relations) {
foreach ($this->$type as $name => $definition) {
if ($this->isAttributePropagatable($name)) {
$this->defineMultisiteRelation($name, $type);
}
}
}
}
/**
* canDeleteMultisiteRelation checks if a relation has the potential to be shared with
* the current model. If there are 2 or more records in existence, then this method
* will prevent the cascading deletion of relations.
*
* @see \October\Rain\Database\Concerns\HasRelationships::performDeleteOnRelations
*/
public function canDeleteMultisiteRelation($name, $type = null): bool
{
// Attribute is exclusive to parent model without propagation
if (!$this->isAttributePropagatable($name)) {
return true;
}
if ($type === null) {
$type = $this->getRelationType($name);
}
// Type is not supported by multisite
if (!in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany', 'belongsTo', 'hasOne', 'hasMany', 'attachOne', 'attachMany'])) {
return true;
}
// The current record counts for one so halt if we find more
return !($this->newOtherSiteQuery()->count() > 1);
}
/**
* defineMultisiteRelation will modify defined relations on this model so they share
* their association using the shared identifier (`site_root_id`). Only these relation
* types support relation sharing: `belongsToMany`, `morphToMany`, `morphedByMany`,
* `belongsTo`, `hasOne`, `hasMany`, `attachOne`, `attachMany`.
*
* For many-to-many relations where both parent AND related models use multisite
* (dual-multisite), the keys remain as default 'id' and propagation handles syncing
* the correct site-specific related records.
*/
protected function defineMultisiteRelation($name, $type = null)
{
if ($type === null) {
$type = $this->getRelationType($name);
}
if ($type) {
if (!is_array($this->$type[$name])) {
$this->$type[$name] = (array) $this->$type[$name];
}
// Override the local key to the shared root identifier
if (in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {
// Check if related model also uses multisite (dual-multisite scenario)
// In dual-multisite, keys stay as default 'id' and pivotSiteScope handles filtering
$relatedIsMultisite = $this->isRelatedMultisite($name);
if ($relatedIsMultisite) {
// Dual-multisite: pivot queries should be scoped by site_id
$this->$type[$name]['pivotSiteScope'] = true;
}
else {
// Single-multisite: use site_root_id to share relations across sites
$this->$type[$name]['parentKey'] = 'site_root_id';
}
}
elseif (in_array($type, ['belongsTo', 'hasOne', 'hasMany'])) {
$this->$type[$name]['otherKey'] = 'site_root_id';
}
elseif (in_array($type, ['attachOne', 'attachMany'])) {
$this->$type[$name]['key'] = 'site_root_id';
}
}
}
/**
* savePropagate the model, including to other sites
* @return bool
*/
public function savePropagate($options = null, $sessionKey = null)
{
return $this->saveInternal((array) $options + ['propagate' => true, 'sessionKey' => $sessionKey]);
}
/**
* addPropagatable attributes for the model.
* @param array|string|null $attributes
*/
public function addPropagatable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->propagatable = array_merge($this->propagatable, $attributes);
foreach ($attributes as $attribute) {
$this->defineMultisiteRelation($attribute);
}
}
/**
* isAttributePropagatable
* @return bool
*/
public function isAttributePropagatable($attribute)
{
return in_array($attribute, $this->propagatable);
}
/**
* propagateToSite will save propagated fields to other records
*/
public function propagateToSite($siteId, $otherModel = null)
{
if ($this->isModelUsingSameSite($siteId)) {
return;
}
if ($otherModel === null) {
$otherModel = $this->findOtherSiteModel($siteId);
}
// Perform propagation for existing records
if ($otherModel->exists) {
foreach ($this->propagatable as $name) {
$relationType = $this->getRelationType($name);
// Propagate local key relation
if ($relationType === 'belongsTo') {
$fkName = $this->$name()->getForeignKeyName();
$otherModel->$fkName = $this->$fkName;
}
// Propagate local attribute (not a relation)
elseif (!$relationType) {
$otherModel->$name = $this->$name;
}
}
}
$otherModel->save(['force' => true]);
// Propagate many-to-many relations after save since pivot
// records require the model to have an ID
foreach ($this->propagatable as $name) {
$relationType = $this->getRelationType($name);
if (in_array($relationType, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {
$this->propagateManyToManyRelation($name, $siteId, $otherModel);
}
}
return $otherModel;
}
/**
* propagateManyToManyRelation propagates a many-to-many relation to another site.
* For dual-multisite (both models use multisite), this finds the corresponding
* related records in the target site and syncs them.
*/
protected function propagateManyToManyRelation($name, $siteId, $otherModel)
{
$relation = $this->$name();
$relatedModel = $relation->getRelated();
// Check if related model uses multisite (dual-multisite scenario)
$relatedIsMultisite = $relatedModel->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class)
&& $relatedModel->isMultisiteEnabled();
if (!$relatedIsMultisite) {
return;
}
// Get related site_root_ids from current model's related records
$relatedRootIds = $relation->pluck('site_root_id')->all();
if (empty($relatedRootIds)) {
// Clear relations on target if source has none
Site::withContext($siteId, function() use ($otherModel, $name) {
$otherModel->$name()->sync([]);
});
return;
}
// Find target site's corresponding records by site_root_id
$targetIds = $relatedModel->newQueryWithoutScopes()
->whereIn('site_root_id', $relatedRootIds)
->where('site_id', $siteId)
->pluck('id')
->all();
// Sync on target model within site context
Site::withContext($siteId, function() use ($otherModel, $name, $targetIds) {
$otherModel->$name()->sync($targetIds);
});
}
/**
* getMultisiteKey returns the root key if multisite is used
*/
public function getMultisiteKey()
{
if (!$this->isMultisiteEnabled()) {
return $this->getKey();
}
return $this->site_root_id ?: $this->getKey();
}
/**
* isMultisiteEnabled allows for programmatic toggling
* @return bool
*/
public function isMultisiteEnabled()
{
return true;
}
/**
* isRelatedMultisite checks if a related model class uses multisite.
* This checks that multisite is enabled via the MultisiteInterface.
*/
protected function isRelatedMultisite($name): bool
{
$relation = $this->getRelationDefinition($name);
$relatedClass = $relation[0] ?? null;
if (!$relatedClass || !class_exists($relatedClass)) {
return false;
}
$relatedModel = new $relatedClass;
return $relatedModel->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class)
&& $relatedModel->isMultisiteEnabled();
}
/**
* isMultisiteSyncEnabled
*/
public function isMultisiteSyncEnabled()
{
if (!property_exists($this, 'propagatableSync')) {
return false;
}
if (is_array($this->propagatableSync)) {
return ($this->propagatableSync['sync'] ?? false) !== false;
}
return (bool) $this->propagatableSync;
}
/**
* getMultisiteConfig
*/
public function getMultisiteConfig($key, $default = null)
{
if (!property_exists($this, 'propagatableSync') || !is_array($this->propagatableSync)) {
return $default;
}
return Arr::get($this->propagatableSync, $key, $default);
}
/**
* getMultisiteSyncSites
* @return array
*/
public function getMultisiteSyncSites()
{
if ($this->getMultisiteConfig('sync') === 'all') {
return Site::listSiteIds();
}
$siteId = $this->{$this->getSiteIdColumn()} ?: null;
if ($this->getMultisiteConfig('sync') === 'locale') {
return Site::listSiteIdsInLocale($siteId);
}
return Site::listSiteIdsInGroup($siteId);
}
/**
* scopeApplyOtherSiteRoot is used to resolve a model using its ID or its root ID.
* For example, finding a model using attributes from another site, or finding
* all connected models for all sites.
*
* If the value is provided as a string, it must be the ID from the primary record,
* in other words: taken from `site_root_id` not from the `id` column.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string|\Illuminate\Database\Eloquent\Model $idOrModel
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeApplyOtherSiteRoot($query, $idOrModel)
{
if ($idOrModel instanceof \Illuminate\Database\Eloquent\Model) {
$idOrModel = $idOrModel->site_root_id ?: $idOrModel->id;
}
return $query->where(function($q) use ($idOrModel) {
$q->where('id', $idOrModel);
$q->orWhere('site_root_id', $idOrModel);
});
}
/**
* newOtherSiteQuery
*/
public function newOtherSiteQuery()
{
return $this->newQueryWithoutScopes()->applyOtherSiteRoot($this);
}
/**
* findForSite will locate a record for a specific site.
*/
public function findForSite($siteId = null)
{
return $this
->newOtherSiteQuery()
->where($this->getSiteIdColumn(), $siteId)
->first();
}
/**
* findOrCreateForSite
*/
public function findOrCreateForSite($siteId = null)
{
$otherModel = $this->findOtherSiteModel($siteId);
// Newly created model
if (!$otherModel->exists) {
$otherModel->save(['force' => true]);
}
// Restoring a trashed model
if (
$otherModel->isClassInstanceOf(\October\Contracts\Database\SoftDeleteInterface::class) &&
$otherModel->trashed()
) {
$otherModel->restore();
}
return $otherModel;
}
/**
* findOtherSiteModel
*/
protected function findOtherSiteModel($siteId = null)
{
if ($siteId === null) {
$siteId = Site::getSiteIdFromContext();
}
if ($this->isModelUsingSameSite($siteId)) {
return $this;
}
$otherModel = $this->findForSite($siteId);
// Replicate without save
if (!$otherModel) {
$otherModel = $this->replicateWithRelations($this->getMultisiteConfig('except'));
$otherModel->{$this->getSiteIdColumn()} = $siteId;
$otherModel->site_root_id = $this->site_root_id ?: $this->id;
}
return $otherModel;
}
/**
* deleteForSite runs the delete command on a model for another site, useful for cleaning
* up records for other sites when the parent is deleted.
*/
public function deleteForSite($siteId = null)
{
$otherModel = $this->findForSite($siteId);
if (!$otherModel) {
return;
}
$useSoftDeletes = $this->isClassInstanceOf(\October\Contracts\Database\SoftDeleteInterface::class);
if ($useSoftDeletes && !$this->isSoftDelete()) {
static::withoutEvents(function() use ($otherModel) {
$otherModel->forceDelete();
});
return;
}
static::withoutEvents(function() use ($otherModel) {
$otherModel->delete();
});
}
/**
* isModelUsingSameSite
*/
protected function isModelUsingSameSite($siteId = null)
{
return (int) $this->{$this->getSiteIdColumn()} === (int) $siteId;
}
/**
* getSiteIdColumn gets the name of the "site id" column.
* @return string
*/
public function getSiteIdColumn()
{
return defined('static::SITE_ID') ? static::SITE_ID : 'site_id';
}
/**
* getQualifiedSiteIdColumn gets the fully qualified "site id" column.
* @return string
*/
public function getQualifiedSiteIdColumn()
{
return $this->qualifyColumn($this->getSiteIdColumn());
}
}
================================================
FILE: src/Database/Traits/MultisiteGroup.php
================================================
bindEvent('model.beforeSave', [$this, 'multisiteGroupBeforeSave']);
}
/**
* multisiteGroupBeforeSave sets the site_group_id from context
*/
public function multisiteGroupBeforeSave()
{
if (Site::hasGlobalContext()) {
return;
}
$this->{$this->getSiteGroupIdColumn()} = Site::getSiteGroupIdFromContext();
}
/**
* isMultisiteGroupEnabled allows for programmatic toggling
*/
public function isMultisiteGroupEnabled(): bool
{
return true;
}
/**
* getSiteGroupIdColumn gets the name of the "site group id" column.
*/
public function getSiteGroupIdColumn(): string
{
return defined('static::SITE_GROUP_ID') ? static::SITE_GROUP_ID : 'site_group_id';
}
/**
* getQualifiedSiteGroupIdColumn gets the fully qualified "site group id" column.
*/
public function getQualifiedSiteGroupIdColumn(): string
{
return $this->qualifyColumn($this->getSiteGroupIdColumn());
}
}
================================================
FILE: src/Database/Traits/NestedTree.php
================================================
integer('parent_id')->nullable();
* $table->integer('nest_left')->nullable();
* $table->integer('nest_right')->nullable();
* $table->integer('nest_depth')->nullable();
*
* You can change the column names used by declaring:
*
* const PARENT_ID = 'my_parent_column';
* const NEST_LEFT = 'my_left_column';
* const NEST_RIGHT = 'my_right_column';
* const NEST_DEPTH = 'my_depth_column';
*
* General access methods:
*
* $model->getRoot(); // Returns the highest parent of a node.
* $model->getRootList(); // Returns an indented array of key and value columns from root.
* $model->getParent(); // The direct parent node.
* $model->getParents(); // Returns all parents up the tree.
* $model->getParentsAndSelf(); // Returns all parents up the tree and self.
* $model->getChildren(); // Set of all direct child nodes.
* $model->getSiblings(); // Return all siblings (parent's children).
* $model->getSiblingsAndSelf(); // Return all siblings and self.
* $model->getLeaves(); // Returns all final nodes without children.
* $model->getDepth(); // Returns the depth of a current node.
* $model->getChildCount(); // Returns number of all children.
*
* Query builder methods:
*
* $query->withoutNode(); // Filters a specific node from the results.
* $query->withoutSelf(); // Filters current node from the results.
* $query->withoutRoot(); // Filters root from the results.
* $query->children(); // Filters as direct children down the tree.
* $query->allChildren(); // Filters as all children down the tree.
* $query->parent(); // Filters as direct parent up the tree.
* $query->parents(); // Filters as all parents up the tree.
* $query->siblings(); // Filters as all siblings (parent's children).
* $query->leaves(); // Filters as all final nodes without children.
* $query->getNested(); // Returns an eager loaded collection of results.
* $query->listsNested(); // Returns an indented array of key and value columns.
*
* Flat result access methods:
*
* $model->getAll(); // Returns everything in correct order.
* $model->getAllRoot(); // Returns all root nodes.
* $model->getAllChildren(); // Returns all children down the tree.
* $model->getAllChildrenAndSelf(); // Returns all children and self.
*
* Eager loaded access methods:
*
* $model->getEagerRoot(); // Returns a list of all root nodes, with ->children eager loaded.
* $model->getEagerChildren(); // Returns direct child nodes, with ->children eager loaded.
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait NestedTree
{
/**
* @var int moveToNewParentId indicates if the model should be aligned to new parent.
*/
protected $moveToNewParentId = false;
/**
* bootNestedTree constructor
*/
public static function bootNestedTree()
{
static::addGlobalScope(new NestedTreeScope);
}
/**
* initializeNestedTree constructor
*/
public function initializeNestedTree()
{
// Define relationships
$this->hasMany['children'] = [
static::class,
'key' => $this->getParentColumnName(),
'replicate' => false
];
$this->belongsTo['parent'] = [
static::class,
'key' => $this->getParentColumnName(),
'replicate' => false
];
// Bind events
$this->bindEvent('model.beforeCreate', function () {
$this->setDefaultLeftAndRight();
});
$this->bindEvent('model.beforeSave', function () {
$this->storeNewParent();
});
$this->bindEvent('model.afterSave', function () {
$this->moveToNewParent();
});
$this->bindEvent('model.beforeDelete', function () {
$this->deleteDescendants();
});
$this->bindEvent('model.beforeRestore', function () {
$this->shiftSiblingsForRestore();
});
$this->bindEvent('model.afterRestore', function () {
$this->restoreDescendants();
});
}
/**
* storeNewParent handles if the parent column is modified so it can be realigned.
*/
public function storeNewParent()
{
// Has the parent column been set from the outside
$parentId = $this->getParentId();
$parentOld = $this->getOriginal($this->getParentColumnName());
$isDirty = (int) $parentOld !== (int) $parentId;
// The parent ID column is nullable, including zero values,
// skipping this logic if the parent ID is already null.
if (!$parentId && $parentId !== null) {
$this->setAttribute($this->getParentColumnName(), null);
$parentId = null;
}
// Parent is not set or unchanged
if (!$isDirty) {
$this->moveToNewParentId = false;
}
// Created as a root node
elseif (!$this->exists && !$parentId) {
$this->moveToNewParentId = false;
}
// Parent has been set
else {
$this->moveToNewParentId = $parentId;
}
}
/**
* moveToNewParent will realign the nesting if the parent identifier is dirty.
*/
public function moveToNewParent()
{
$parentId = $this->moveToNewParentId;
if ($parentId === false) {
return;
}
if ($parentId === null) {
$this->makeRoot();
return;
}
$parentModel = $this->resolveMoveTarget($parentId);
if ($parentModel) {
$this->makeChildOf($parentModel);
return;
}
// Nullify parent since nothing valid was found
$this->newNestedTreeQuery()
->where($this->getKeyName(), $this->getKey())
->update([$this->getParentColumnName() => null]);
}
/**
* deleteDescendants deletes a branch off the tree, shifting all the elements on the right
* back to the left so the counts work.
*/
public function deleteDescendants()
{
if ($this->getRight() === null || $this->getLeft() === null) {
return;
}
$this->getConnection()->transaction(function () {
$this->reload();
$leftCol = $this->getLeftColumnName();
$rightCol = $this->getRightColumnName();
$left = $this->getLeft();
$right = $this->getRight();
// Delete children
$this->newNestedTreeQuery()
->where($leftCol, '>', $left)
->where($rightCol, '<', $right)
->delete()
;
// Update left and right indexes for the remaining nodes
$diff = $right - $left + 1;
$this->newNestedTreeQuery()
->where($leftCol, '>', $right)
->decrement($leftCol, $diff)
;
$this->newNestedTreeQuery()
->where($rightCol, '>', $right)
->decrement($rightCol, $diff)
;
});
}
/**
* shiftSiblingsForRestore allocates a slot for the the current node between its siblings.
*/
public function shiftSiblingsForRestore()
{
if ($this->getRight() === null || $this->getLeft() === null) {
return;
}
$this->getConnection()->transaction(function () {
$leftCol = $this->getLeftColumnName();
$rightCol = $this->getRightColumnName();
$left = $this->getLeft();
$right = $this->getRight();
// Update left and right indexes for the remaining nodes
$diff = $right - $left + 1;
$this->newNestedTreeQuery()
->where($leftCol, '>=', $left)
->increment($leftCol, $diff)
;
$this->newNestedTreeQuery()
->where($rightCol, '>=', $left)
->increment($rightCol, $diff)
;
});
}
/**
* restoreDescendants of the current node.
*/
public function restoreDescendants()
{
if ($this->getRight() === null || $this->getLeft() === null) {
return;
}
$this->getConnection()->transaction(function () {
$this->newNestedTreeQuery()
->withTrashed()
->where($this->getLeftColumnName(), '>', $this->getLeft())
->where($this->getRightColumnName(), '<', $this->getRight())
->update([
$this->getDeletedAtColumn() => null,
$this->getUpdatedAtColumn() => $this->{$this->getUpdatedAtColumn()}
])
;
});
}
//
// Alignment
//
/**
* makeRoot makes this model a root node.
* @return \October\Rain\Database\Model
*/
public function makeRoot()
{
return $this->moveAfter($this->getRoot());
}
/**
* makeChildOf makes model node a child of specified node.
* @return \October\Rain\Database\Model
*/
public function makeChildOf($node)
{
return $this->moveTo($node, 'child');
}
/**
* moveLeft finds the left sibling and move to left of it.
* @return \October\Rain\Database\Model
*/
public function moveLeft()
{
return $this->moveBefore($this->getLeftSibling());
}
/**
* moveRight finds the right sibling and move to the right of it.
* @return \October\Rain\Database\Model
*/
public function moveRight()
{
return $this->moveAfter($this->getRightSibling());
}
/**
* moveBefore moves to the model to before (left) specified node.
* @return \October\Rain\Database\Model
*/
public function moveBefore($node)
{
return $this->moveTo($node, 'left');
}
/**
* moveAfter moves to the model to after (right) a specified node.
* @return \October\Rain\Database\Model
*/
public function moveAfter($node)
{
return $this->moveTo($node, 'right');
}
//
// Checkers
//
/**
* isRoot returns true if this is a root node.
*/
public function isRoot(): bool
{
return $this->getParentId() === null;
}
/**
* isChild returns true if this is a child node.
*/
public function isChild(): bool
{
return !$this->isRoot();
}
/**
* isLeaf returns true if this is a leaf node (end of a branch).
*/
public function isLeaf(): bool
{
return $this->exists && ($this->getRight() - $this->getLeft() === 1);
}
/**
* isInsideSubtree checks if the supplied node is inside the subtree of this model.
* @param \Model
*/
public function isInsideSubtree($node): bool
{
return (
$this->getLeft() >= $node->getLeft() &&
$this->getLeft() <= $node->getRight() &&
$this->getRight() >= $node->getLeft() &&
$this->getRight() <= $node->getRight()
);
}
/**
* isDescendantOf returns true if node is a descendant.
* @param NestedSet
*/
public function isDescendantOf($other): bool
{
return ($this->getLeft() > $other->getLeft() && $this->getLeft() < $other->getRight());
}
//
// Scopes
//
/**
* scopeWithoutNode extracts a certain node object from the current query expression.
* @return \Illuminate\Database\Query\Builder
*/
public function scopeWithoutNode($query, $node)
{
return $query->where($node->getKeyName(), '!=', $node->getKey());
}
/**
* scopeWithoutSelf extracts current node (self) from current query expression.
* @return \Illuminate\Database\Query\Builder
*/
public function scopeWithoutSelf($query)
{
return $this->scopeWithoutNode($query, $this);
}
/**
* scopeWithoutRoot extracts first root (from the current node context) from current
* query expression.
* @return \Illuminate\Database\Query\Builder
*/
public function scopeWithoutRoot($query)
{
return $this->scopeWithoutNode($query, $this->getRoot());
}
/**
* scopeAllChildren sets of all children & nested children.
* @return \Illuminate\Database\Query\Builder
*/
public function scopeAllChildren($query, $includeSelf = false)
{
$query
->where($this->getLeftColumnName(), '>=', $this->getLeft())
->where($this->getLeftColumnName(), '<', $this->getRight())
;
return $includeSelf ? $query : $query->withoutSelf();
}
/**
* scopeParents returns a prepared query with all parents up the tree.
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeParents($query, $includeSelf = false)
{
$query
->where($this->getLeftColumnName(), '<=', $this->getLeft())
->where($this->getRightColumnName(), '>=', $this->getRight())
;
return $includeSelf ? $query : $query->withoutSelf();
}
/**
* scopeSiblings filters targeting all children of the parent, except self.
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSiblings($query, $includeSelf = false)
{
$query->where($this->getParentColumnName(), $this->getParentId());
return $includeSelf ? $query : $query->withoutSelf();
}
/**
* scopeLeaves returns all final nodes without children.
* @return \Illuminate\Database\Query\Builder
*/
public function scopeLeaves($query)
{
$grammar = $this->getConnection()->getQueryGrammar();
$rightCol = $grammar->wrap($this->getQualifiedRightColumnName());
$leftCol = $grammar->wrap($this->getQualifiedLeftColumnName());
return $query
->allChildren()
->whereRaw($rightCol . ' - ' . $leftCol . ' = 1')
;
}
/**
* scopeGetAllRoot returns a list of all root nodes, without eager loading.
* @return \October\Rain\Database\Collection
*/
public function scopeGetAllRoot($query)
{
return $query
->where(function ($query) {
$query->whereNull($this->getParentColumnName());
$query->orWhere($this->getParentColumnName(), 0);
})
->get()
;
}
/**
* scopeGetNested is a non-chaining scope, returns an eager loaded hierarchy tree.
* Children are eager loaded inside the $model->children relation.
* @return Collection A collection
*/
public function scopeGetNested($query)
{
return $query->get()->toNested();
}
/**
* scopeListsNested gets an array with values of a given column. Values are indented
* according to their depth.
* @param string $column Array values
* @param string $key Array keys
* @param string $indent Character to indent depth
* @return array
*/
public function scopeListsNested($query, $column, $key = null, $indent = ' ')
{
$resultKeyName = $this->getKeyName();
$columns = [$this->getDepthColumnName(), $this->getParentColumnName(), $this->getKeyName(), $column];
if ($key !== null) {
$resultKeyName = $key;
$columns[] = $key;
}
$values = $parentIds = [];
$results = $query->orderBy($this->getLeftColumnName())->get($columns);
foreach ($results as $result) {
$parentId = $result->{$this->getParentColumnName()};
if ($parentId && !isset($parentIds[$parentId])) {
continue;
}
$parentIds[$result->{$this->getKeyName()}] = true;
$values[$result->{$resultKeyName}] = str_repeat(
$indent,
$result->{$this->getDepthColumnName()}
) . $result->{$column};
}
return $values;
}
//
// Getters
//
/**
* getAll returns all nodes and children.
* @return \October\Rain\Database\Collection
*/
public function getAll($columns = ['*'])
{
return $this->newNestedTreeQuery()->get($columns);
}
/**
* getRoot returns the root node starting from the current node.
* @return \October\Rain\Database\Model
*/
public function getRoot()
{
if ($this->exists) {
return $this->newNestedTreeQuery()->parents(true)
->where(function ($query) {
$query->whereNull($this->getParentColumnName());
$query->orWhere($this->getParentColumnName(), 0);
})
->first()
;
}
$parentId = $this->getParentId();
if ($parentId !== null && ($currentParent = $this->newNestedTreeQuery()->find($parentId))) {
return $currentParent->getRoot();
}
return $this;
}
/**
* getEagerRoot returns a list of all root nodes, with children eager loaded.
* @return \October\Rain\Database\Collection
*/
public function getEagerRoot()
{
return $this->newNestedTreeQuery()->getNested();
}
/**
* getRootList returns an array column/key pair of all root nodes, with children eager loaded.
* @return array
*/
public function getRootList($column, $key = null, $indent = ' ')
{
return $this->newNestedTreeQuery()->listsNested($column, $key, $indent);
}
/**
* getParent returns the direct parent node.
* @return \October\Rain\Database\Collection
*/
public function getParent()
{
return $this->parent()->get();
}
/**
* getParents returns all parents up the tree.
* @return \October\Rain\Database\Collection
*/
public function getParents()
{
return $this->newNestedTreeQuery()->parents()->get();
}
/**
* getParentsAndSelf returns all parents up the tree and self.
* @return \October\Rain\Database\Collection
*/
public function getParentsAndSelf()
{
return $this->newNestedTreeQuery()->parents(true)->get();
}
/**
* getChildren returns direct child nodes.
* @return \October\Rain\Database\Collection
*/
public function getChildren()
{
return $this->children;
}
/**
* getEagerChildren returns direct child nodes, with ->children eager loaded.
* @return \October\Rain\Database\Collection
*/
public function getEagerChildren()
{
return $this->newNestedTreeQuery()->allChildren()->getNested();
}
/**
* getAllChildren returns all children down the tree.
* @return \October\Rain\Database\Collection
*/
public function getAllChildren()
{
return $this->newNestedTreeQuery()->allChildren()->get();
}
/**
* getAllChildrenAndSelf returns all children and self.
* @return \October\Rain\Database\Collection
*/
public function getAllChildrenAndSelf()
{
return $this->newNestedTreeQuery()->allChildren(true)->get();
}
/**
* getSiblings returns all siblings (parent's children).
* @return \October\Rain\Database\Collection
*/
public function getSiblings()
{
return $this->newNestedTreeQuery()->siblings()->get();
}
/**
* getSiblingsAndSelf
* @return \October\Rain\Database\Collection
*/
public function getSiblingsAndSelf()
{
return $this->newNestedTreeQuery()->siblings(true)->get();
}
/**
* getLeftSibling
* @return \October\Rain\Database\Model
*/
public function getLeftSibling()
{
return $this->siblings()->where($this->getRightColumnName(), $this->getLeft() - 1)->first();
}
/**
* getRightSibling
* @return \October\Rain\Database\Model
*/
public function getRightSibling()
{
return $this->siblings()->where($this->getLeftColumnName(), $this->getRight() + 1)->first();
}
/**
* getLeaves returns all final nodes without children.
* @return \October\Rain\Database\Collection
*/
public function getLeaves()
{
return $this->newNestedTreeQuery()->leaves()->get();
}
/**
* getLevel returns the level of this node in the tree. Root level is 0.
* @return int
*/
public function getLevel()
{
if ($this->getParentId() === null) {
return 0;
}
return $this->newNestedTreeQuery()->parents()->count();
}
/**
* getChildCount returns number of all children below it.
* @return int
*/
public function getChildCount()
{
return ($this->getRight() - $this->getLeft() - 1) / 2;
}
//
// Setters
//
/**
* setDepth sets the depth attribute.
* @return \October\Rain\Database\Model
*/
public function setDepth()
{
$this->getConnection()->transaction(function () {
$this->reload();
$level = $this->getLevel();
$this->newNestedTreeQuery()
->where($this->getKeyName(), $this->getKey())
->update([$this->getDepthColumnName() => $level])
;
$this->setAttribute($this->getDepthColumnName(), $level);
});
return $this;
}
/**
* setDefaultLeftAndRight columns
* @return void
*/
public function setDefaultLeftAndRight()
{
$highRight = $this
->newNestedTreeQuery()
->reorder()
->orderBy($this->getRightColumnName(), 'desc')
->limit(1)
->first()
;
$maxRight = 0;
if ($highRight !== null) {
$maxRight = $highRight->getRight();
}
$this->setAttribute($this->getLeftColumnName(), $maxRight + 1);
$this->setAttribute($this->getRightColumnName(), $maxRight + 2);
$this->setAttribute($this->getDepthColumnName(), 0);
}
/**
* resetTreeNesting can be used to repair corrupt or missing tree definitions,
* it will flatten and heal the necessary columns, all parent and child
* associations are retained.
*/
public function resetTreeNesting()
{
$this->getConnection()->transaction(function () {
$buildFunc = function($items, &$nest, $level = 0) use (&$buildFunc) {
$items->each(function ($item) use (&$nest, $level, $buildFunc) {
$item->setAttribute($this->getLeftColumnName(), $nest++);
$item->setAttribute($this->getDepthColumnName(), $level);
$buildFunc($item->getChildren(), $nest, $level + 1);
$item->setAttribute($this->getRightColumnName(), $nest++);
$item->save(['force' => true]);
});
};
$records = $this
->newNestedTreeQuery()
->whereNull($this->getParentColumnName())
->get()
;
$nest = 1;
$buildFunc($records, $nest);
});
}
/**
* resetTreeOrphans can be used to locate orphaned records, those that refer
* to a parent_id value where the associated record no longer exists, and
* promote them to be visible in the collection again, by setting the
* parent column to null.
*/
public function resetTreeOrphans()
{
$orphanMap = [];
$recordMap = $this
->newNestedTreeQuery()
->pluck($this->getParentColumnName(), $this->getKeyName())
->all();
foreach ($recordMap as $id => $parent) {
if ($parent && !array_key_exists($parent, $recordMap)) {
$orphanMap[] = $id;
}
}
if ($orphanMap) {
$this->newNestedTreeQuery()
->whereIn($this->getKeyName(), $orphanMap)
->update([$this->getParentColumnName() => null]);
}
}
//
// Moving
//
/**
* moveTo is a handler for all node alignments.
* @param mixed $target
* @param string $position
* @return \October\Rain\Database\Model
*/
protected function moveTo($target, $position)
{
// Validate target
if ($target instanceof \October\Rain\Database\Model) {
$target->reload();
}
else {
$target = $this->resolveMoveTarget($target);
}
// Validate move
if (!$this->validateMove($this, $target, $position)) {
return $this;
}
// Perform move
$this->getConnection()->transaction(function () use ($target, $position) {
$this->performMove($this, $target, $position);
});
// Reapply alignments
$target->reload();
$this->setDepth();
foreach ($this->newNestedTreeQuery()->allChildren()->get() as $descendant) {
$descendant->setDepth();
}
$this->reload();
return $this;
}
/**
* performMove executes the SQL query associated with the update of the indexes affected
* by the move operation.
* @return int
*/
protected function performMove($node, $target, $position)
{
[$a, $b, $c, $d] = $this->getSortedBoundaries($node, $target, $position);
$connection = $node->getConnection();
$grammar = $connection->getQueryGrammar();
$pdo = $connection->getPdo();
$parentId = $position === 'child'
? $target->getKey()
: $target->getParentId();
if ($parentId === null) {
$parentId = 'NULL';
}
else {
$parentId = $pdo->quote($parentId);
}
$currentId = $pdo->quote($node->getKey());
$leftColumn = $node->getLeftColumnName();
$rightColumn = $node->getRightColumnName();
$parentColumn = $node->getParentColumnName();
$wrappedLeft = $grammar->wrap($leftColumn);
$wrappedRight = $grammar->wrap($rightColumn);
$wrappedParent = $grammar->wrap($parentColumn);
$wrappedId = DbDongle::cast($grammar->wrap($node->getKeyName()), 'TEXT');
$leftSql = "CASE
WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b
WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c
ELSE $wrappedLeft END";
$rightSql = "CASE
WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b
WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c
ELSE $wrappedRight END";
$parentSql = "CASE
WHEN $wrappedId = $currentId THEN $parentId
ELSE $wrappedParent END";
$result = $node->newNestedTreeQuery()
->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) {
$query
->whereBetween($leftColumn, [$a, $d])
->orWhereBetween($rightColumn, [$a, $d])
;
})
->update([
$leftColumn => $connection->raw($leftSql),
$rightColumn => $connection->raw($rightSql),
$parentColumn => $connection->raw($parentSql)
])
;
return $result;
}
/**
* resolveMoveTarget
* @return \October\Rain\Database\Model|null
*/
protected function resolveMoveTarget($targetId)
{
$query = $this->newNestedTreeQuery();
if (
$this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$this->isMultisiteEnabled()
) {
return $query->applyOtherSiteRoot($targetId)->first();
}
return $query->find($targetId);
}
/**
* validateMove validates a proposed move and returns true if changes are needed.
* @return void
*/
protected function validateMove($node, $target, $position)
{
if (!$node->exists) {
throw new Exception('A new node cannot be moved.');
}
if (!in_array($position, ['child', 'left', 'right'])) {
throw new Exception(sprintf(
'Position should be either child, left, right. Supplied position is "%s".',
$position
));
}
if ($target === null) {
if ($position === 'left' || $position === 'right') {
throw new Exception(sprintf(
'Cannot resolve target node. This node cannot move any further to the %s.',
$position
));
}
throw new Exception('Cannot resolve target node.');
}
if ($node === $target) {
throw new Exception('A node cannot be moved to itself.');
}
if ($target->isInsideSubtree($node)) {
throw new Exception('A node cannot be moved to a descendant of itself.');
}
return !(
$this->getPrimaryBoundary($node, $target, $position) === $node->getRight() ||
$this->getPrimaryBoundary($node, $target, $position) === $node->getLeft()
);
}
/**
* getPrimaryBoundary calculates the boundary.
* @return int
*/
protected function getPrimaryBoundary($node, $target, $position)
{
$primaryBoundary = null;
switch ($position) {
case 'child':
$primaryBoundary = $target->getRight();
break;
case 'left':
$primaryBoundary = $target->getLeft();
break;
case 'right':
$primaryBoundary = $target->getRight() + 1;
break;
}
return ($primaryBoundary > $node->getRight())
? $primaryBoundary - 1
: $primaryBoundary;
}
/**
* getOtherBoundary calculates the other boundary.
* @return int
*/
protected function getOtherBoundary($node, $target, $position)
{
return ($this->getPrimaryBoundary($node, $target, $position) > $node->getRight())
? $node->getRight() + 1
: $node->getLeft() - 1;
}
/**
* getSortedBoundaries calculates a sorted boundaries array.
* @return array
*/
protected function getSortedBoundaries($node, $target, $position)
{
$boundaries = [
$node->getLeft(),
$node->getRight(),
$this->getPrimaryBoundary($node, $target, $position),
$this->getOtherBoundary($node, $target, $position)
];
sort($boundaries);
return $boundaries;
}
//
// Column getters
//
/**
* getParentColumnName
* @return string
*/
public function getParentColumnName()
{
return defined('static::PARENT_ID') ? static::PARENT_ID : 'parent_id';
}
/**
* getQualifiedParentColumnName
* @return string
*/
public function getQualifiedParentColumnName()
{
return $this->getTable(). '.' .$this->getParentColumnName();
}
/**
* getParentId gets value of the model parent_id column.
* @return int
*/
public function getParentId()
{
return $this->getAttribute($this->getParentColumnName());
}
/**
* getLeftColumnName
* @return string
*/
public function getLeftColumnName()
{
return defined('static::NEST_LEFT') ? static::NEST_LEFT : 'nest_left';
}
/**
* getQualifiedLeftColumnName
* @return string
*/
public function getQualifiedLeftColumnName()
{
return $this->getTable() . '.' . $this->getLeftColumnName();
}
/**
* getLeft column value.
* @return int
*/
public function getLeft()
{
return $this->getAttribute($this->getLeftColumnName());
}
/**
* getRightColumnName
* @return string
*/
public function getRightColumnName()
{
return defined('static::NEST_RIGHT') ? static::NEST_RIGHT : 'nest_right';
}
/**
* getQualifiedRightColumnName
* @return string
*/
public function getQualifiedRightColumnName()
{
return $this->getTable() . '.' . $this->getRightColumnName();
}
/**
* getRight column value.
* @return int
*/
public function getRight()
{
return $this->getAttribute($this->getRightColumnName());
}
/**
* getDepthColumnName
* @return string
*/
public function getDepthColumnName()
{
return defined('static::NEST_DEPTH') ? static::NEST_DEPTH : 'nest_depth';
}
/**
* getQualifiedDepthColumnName
* @return string
*/
public function getQualifiedDepthColumnName()
{
return $this->getTable() . '.' . $this->getDepthColumnName();
}
/**
* getDepth column value.
* @return int
*/
public function getDepth()
{
return $this->getAttribute($this->getDepthColumnName());
}
//
// Instances
//
/**
* newNestedTreeQuery creates a new query for nested sets
*/
protected function newNestedTreeQuery()
{
return $this->newQuery();
}
/**
* newCollection returns a custom TreeCollection collection.
*/
public function newCollection(array $models = [])
{
return new TreeCollection($models);
}
}
================================================
FILE: src/Database/Traits/Nullable.php
================================================
nullable)) {
throw new Exception(sprintf(
'The $nullable property in %s must be an array to use the Nullable trait.',
static::class
));
}
$this->bindEvent('model.beforeSaveDone', [$this, 'nullableBeforeSave']);
}
/**
* addNullable attribute to the nullable attributes list
*/
public function addNullable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->nullable = array_merge($this->nullable, $attributes);
}
/**
* checkNullableValue checks if the supplied value is empty, excluding zero.
*/
public function checkNullableValue($value): bool
{
if ($value === 0 || $value === '0' || $value === 0.0 || $value === false) {
return false;
}
return empty($value);
}
/**
* nullableBeforeSave will nullify empty fields at time of saving.
*/
public function nullableBeforeSave()
{
foreach ($this->nullable as $field) {
if ($this->checkNullableValue($this->{$field})) {
if ($this->exists) {
$this->attributes[$field] = null;
}
else {
unset($this->attributes[$field]);
}
}
}
}
}
================================================
FILE: src/Database/Traits/Purgeable.php
================================================
purgeable)) {
throw new Exception(sprintf(
'The $purgeable property in %s must be an array to use the Purgeable trait.',
static::class
));
}
$this->bindEvent('model.beforeSaveDone', [$this, 'purgeAttributes']);
}
/**
* addPurgeable adds an attribute to the purgeable attributes list
* @param array|string|null $attributes
* @return void
*/
public function addPurgeable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->purgeable = array_merge($this->purgeable, $attributes);
}
/**
* purgeAttributes removes purged attributes from the dataset, used before saving.
* Specify attributesToPurge, if unspecified, $purgeable property is used
* @param mixed $attributes
* @return array
*/
public function purgeAttributes($attributesToPurge = null)
{
if ($attributesToPurge === null) {
$purgeable = $this->getPurgeableAttributes();
}
else {
$purgeable = (array) $attributesToPurge;
}
$attributes = $this->getAttributes();
$cleanAttributes = array_diff_key($attributes, array_flip($purgeable));
$originalAttributes = array_diff_key($attributes, $cleanAttributes);
$this->originalPurgeableValues = array_merge(
$this->originalPurgeableValues,
$originalAttributes
);
return $this->attributes = $cleanAttributes;
}
/**
* getPurgeableAttributes returns a collection of fields that will be hashed.
*/
public function getPurgeableAttributes()
{
return $this->purgeable;
}
/**
* getOriginalPurgeValues returns the original values of any purged attributes.
*/
public function getOriginalPurgeValues()
{
return $this->originalPurgeableValues;
}
/**
* getOriginalPurgeValue returns the original values of any purged attributes.
*/
public function getOriginalPurgeValue($attribute)
{
return $this->attributes[$attribute]
?? ($this->originalPurgeableValues[$attribute] ?? null);
}
/**
* restorePurgedValues restores the original values of any purged attributes.
*/
public function restorePurgedValues()
{
$this->attributes = array_merge(
$this->getAttributes(),
$this->originalPurgeableValues
);
}
}
================================================
FILE: src/Database/Traits/Revisionable.php
================================================
revisionable)) {
throw new Exception(sprintf(
'The $revisionable property in %s must be an array to use the Revisionable trait.',
static::class
));
}
$this->bindEvent('model.afterUpdate', function () {
$this->revisionableAfterUpdate();
});
$this->bindEvent('model.afterDelete', function () {
$this->revisionableAfterDelete();
});
}
/**
* revisionableAfterUpdate event
*/
public function revisionableAfterUpdate()
{
if (!$this->revisionsEnabled) {
return;
}
$relation = $this->getRevisionHistoryName();
$relationObject = $this->{$relation}();
$revisionModel = $relationObject->getRelated();
$toSave = [];
$dirty = $this->getDirty();
foreach ($dirty as $attribute => $value) {
if (!in_array($attribute, $this->revisionable)) {
continue;
}
$toSave[] = [
'field' => $attribute,
'old_value' => Arr::get($this->original, $attribute),
'new_value' => $value,
'revisionable_type' => $relationObject->getMorphClass(),
'revisionable_id' => $this->getKey(),
'user_id' => $this->revisionableGetUser(),
'cast' => $this->revisionableGetCastType($attribute),
'created_at' => new DateTime,
'updated_at' => new DateTime
];
}
// Nothing to do
if (!count($toSave)) {
return;
}
Db::table($revisionModel->getTable())->insert($toSave);
$this->revisionableCleanUp();
}
/**
* revisionableAfterDelete event
*/
public function revisionableAfterDelete()
{
if (!$this->revisionsEnabled) {
return;
}
$softDeletes = in_array(
\October\Rain\Database\Traits\SoftDelete::class,
class_uses_recursive(static::class)
);
if (!$softDeletes) {
return;
}
if (!in_array('deleted_at', $this->revisionable)) {
return;
}
$relation = $this->getRevisionHistoryName();
$relationObject = $this->{$relation}();
$revisionModel = $relationObject->getRelated();
$toSave = [
'field' => 'deleted_at',
'old_value' => null,
'new_value' => $this->deleted_at,
'revisionable_type' => $relationObject->getMorphClass(),
'revisionable_id' => $this->getKey(),
'user_id' => $this->revisionableGetUser(),
'created_at' => new DateTime,
'updated_at' => new DateTime
];
Db::table($revisionModel->getTable())->insert($toSave);
$this->revisionableCleanUp();
}
/*
* revisionableCleanUp deletes revision records exceeding the limit.
*/
protected function revisionableCleanUp()
{
$relation = $this->getRevisionHistoryName();
$relationObject = $this->{$relation}();
$revisionLimit = property_exists($this, 'revisionableLimit')
? (int) $this->revisionableLimit
: 500;
$toDelete = $relationObject
->orderBy('id', 'desc')
->skip($revisionLimit)
->limit(64)
->get();
foreach ($toDelete as $record) {
$record->delete();
}
}
/**
* revisionableGetCastType
*/
protected function revisionableGetCastType($attribute)
{
if (in_array($attribute, $this->getDates())) {
return 'date';
}
return null;
}
/**
* revisionableGetUser
*/
protected function revisionableGetUser()
{
if (method_exists($this, 'getRevisionableUser')) {
$user = $this->getRevisionableUser();
return $user instanceof EloquentModel
? $user->getKey()
: $user;
}
return null;
}
/**
* getRevisionHistoryName
* @return string
*/
public function getRevisionHistoryName()
{
return defined('static::REVISION_HISTORY') ? static::REVISION_HISTORY : 'revision_history';
}
}
================================================
FILE: src/Database/Traits/SimpleTree.php
================================================
getChildren(); // Returns children of this node
* $model->getChildCount(); // Returns number of all children.
* $model->getAllChildren(); // Returns all children of this node
* $model->getAllRoot(); // Returns all root level nodes (eager loaded)
* $model->getAll(); // Returns everything in correct order.
*
* Query builder methods:
*
* $query->listsNested(); // Returns an indented array of key and value columns.
*
* You can change the sort field used by declaring:
*
* const PARENT_ID = 'my_parent_column';
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait SimpleTree
{
/**
* initializeSimpleTree constructor
*/
public function initializeSimpleTree()
{
// Define relationships
$this->hasMany['children'] = [
static::class,
'key' => $this->getParentColumnName(),
'replicate' => false
];
$this->belongsTo['parent'] = [
static::class,
'key' => $this->getParentColumnName(),
'replicate' => false
];
// Multisite
if (
$this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$this->isMultisiteSyncEnabled() &&
$this->getMultisiteConfig('structure', true)
) {
$this->addPropagatable(['children', 'parent']);
}
}
/**
* getAll returns all nodes and children.
* @return \October\Rain\Database\Collection
*/
public function getAll()
{
$collection = [];
foreach ($this->getAllRoot() as $rootNode) {
$collection[] = $rootNode;
$collection = $collection + $rootNode->getAllChildren()->getDictionary();
}
return new Collection($collection);
}
/**
* getAllChildren gets a list of children records, with their children (recursive)
* @return \October\Rain\Database\Collection
*/
public function getAllChildren()
{
$result = [];
$children = $this->getChildren();
foreach ($children as $child) {
$result[] = $child;
$childResult = $child->getAllChildren();
foreach ($childResult as $subChild) {
$result[] = $subChild;
}
}
return new Collection($result);
}
/**
* getChildren returns direct child nodes.
* @return \October\Rain\Database\Collection
*/
public function getChildren()
{
return $this->children;
}
/**
* getChildCount returns number of all children below it.
* @return int
*/
public function getChildCount()
{
return count($this->getAllChildren());
}
/**
* getParents returns an array of parents, this is a heavy query and can produce
* in multiple queries.
*/
public function getParents()
{
$result = [];
$parent = $this;
$result[] = $parent;
while ($parent = $parent->parent) {
$result[] = $parent;
}
return array_reverse($result);
}
//
// Scopes
//
/**
* scopeGetAllRoot returns a list of all root nodes, without eager loading.
* @return \October\Rain\Database\Collection
*/
public function scopeGetAllRoot($query)
{
return $query->where($this->getParentColumnName(), null)->get();
}
/**
* scopeGetNested is a non-chaining scope, returns an eager loaded hierarchy tree.
* Children are eager loaded inside the $model->children relation.
* @return Collection A collection
*/
public function scopeGetNested($query)
{
return $query->get()->toNested();
}
/**
* scopeListsNested gets an array with values of a given column. Values are indented
* according to their depth.
* @param string $column Array values
* @param string $key Array keys
* @param string $indent Character to indent depth
* @return array
*/
public function scopeListsNested($query, $column, $key = null, $indent = ' ')
{
$idName = $this->getKeyName();
$parentName = $this->getParentColumnName();
$columns = [$idName, $parentName, $column];
if ($key !== null) {
$columns[] = $key;
}
$collection = $query->getQuery()->get($columns);
// Assign all child nodes to their parents
$pairMap = [];
$rootItems = [];
foreach ($collection as $record) {
if ($parentId = $record->{$parentName}) {
if (!isset($pairMap[$parentId])) {
$pairMap[$parentId] = [];
}
$pairMap[$parentId][] = $record;
}
else {
$rootItems[] = $record;
}
}
// Recursive helper function
$buildCollection = function(
$items,
$map,
$depth = 0
) use (
&$buildCollection,
$column,
$key,
$indent,
$idName
) {
$result = [];
$indentString = str_repeat($indent, $depth);
foreach ($items as $item) {
if (!property_exists($item, $column)) {
throw new Exception('Column mismatch in listsNested method. Are you sure the columns exist?');
}
if ($key !== null) {
$result[$item->{$key}] = $indentString . $item->{$column};
}
else {
$result[] = $indentString . $item->{$column};
}
// Add the children
$childItems = Arr::get($map, $item->{$idName}, []);
if (count($childItems) > 0) {
$result = $result + $buildCollection($childItems, $map, $depth + 1);
}
}
return $result;
};
// Build a nested collection
return $buildCollection($rootItems, $pairMap);
}
/**
* getParentColumnName
* @return string
*/
public function getParentColumnName()
{
return defined('static::PARENT_ID') ? static::PARENT_ID : 'parent_id';
}
/**
* getQualifiedParentColumnName
* @return string
*/
public function getQualifiedParentColumnName()
{
return $this->getTable(). '.' .$this->getParentColumnName();
}
/**
* getParentId gets value of the model parent_id column.
* @return int
*/
public function getParentId()
{
return $this->getAttribute($this->getParentColumnName());
}
/**
* newCollection returns a custom TreeCollection collection.
*/
public function newCollection(array $models = [])
{
return new TreeCollection($models);
}
}
================================================
FILE: src/Database/Traits/Sluggable.php
================================================
slugs)) {
throw new Exception(sprintf(
'The $slugs property in %s must be an array to use the Sluggable trait.',
static::class
));
}
// Set slugged attributes on new records and existing records if slug is missing.
$this->bindEvent('model.saveInternal', function () {
$this->slugAttributes();
});
}
/**
* slugAttributes adds slug attributes to the dataset, used before saving.
* @return void
*/
public function slugAttributes()
{
foreach ($this->slugs as $slugAttribute => $sourceAttributes) {
$this->setSluggedValue($slugAttribute, $sourceAttributes);
}
}
/**
* setSluggedValue sets a single slug attribute value, using source attributes
* to generate the slug from and a maximum length for the slug not including
* the counter. Source attributes support dotted notation for relations.
* @param string $slugAttribute
* @param mixed $sourceAttributes
* @param int $maxLength
* @return string
*/
public function setSluggedValue($slugAttribute, $sourceAttributes, $maxLength = 175)
{
if (!array_key_exists($slugAttribute, $this->attributes) || !mb_strlen($this->attributes[$slugAttribute])) {
if (!is_array($sourceAttributes)) {
$sourceAttributes = [$sourceAttributes];
}
$slugArr = [];
foreach ($sourceAttributes as $attribute) {
$slugArr[] = $this->getSluggableSourceAttributeValue($attribute);
}
$slug = implode(' ', $slugArr);
$slug = mb_substr($slug, 0, $maxLength);
$slug = Str::slug($slug, $this->getSluggableSeparator(), App::getLocale());
}
else {
$slug = $this->attributes[$slugAttribute] ?? '';
}
// Source attributes contain empty values, nothing to slug and this
// happens when the attributes are not required by the validator
if (!mb_strlen(trim($slug))) {
return $this->attributes[$slugAttribute] = '';
}
return $this->attributes[$slugAttribute] = $this->getSluggableUniqueAttributeValue($slugAttribute, $slug);
}
/**
* getSluggableUniqueAttributeValue ensures a unique attribute value, if the value is already
* used a counter suffix is added. Returns a safe value that is unique.
* @param string $name
* @param mixed $value
* @return string
*/
protected function getSluggableUniqueAttributeValue($name, $value)
{
$counter = 1;
$separator = $this->getSluggableSeparator();
$_value = $value;
while ($this->newSluggableQuery()->where($name, $_value)->count() > 0) {
$counter++;
$_value = $value . $separator . $counter;
}
return $_value;
}
/**
* getSluggableSourceAttributeValue using dotted notation.
* Eg: author.name
* @return mixed
*/
protected function getSluggableSourceAttributeValue($key)
{
if (strpos($key, '.') === false) {
return $this->getAttribute($key);
}
$keyParts = explode('.', $key);
$value = $this;
foreach ($keyParts as $part) {
if (!isset($value[$part])) {
return null;
}
$value = $value[$part];
}
return $value;
}
/**
* getSluggableSeparator is an override for the default slug separator.
* @return string
*/
public function getSluggableSeparator()
{
return defined('static::SLUG_SEPARATOR') ? static::SLUG_SEPARATOR : '-';
}
/**
* newSluggableQuery returns a query that excludes the current record if it exists
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newSluggableQuery()
{
$query = $this->newQuery();
if ($this->exists) {
$query->where($this->getKeyName(), '<>', $this->getKey());
}
if ($this->isClassInstanceOf(\October\Contracts\Database\SoftDeleteInterface::class)) {
$query->withTrashed();
}
if (
$this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$this->isMultisiteEnabled()
) {
$query->withSite($this->{$this->getSiteIdColumn()});
}
return $query;
}
}
================================================
FILE: src/Database/Traits/SluggableTree.php
================================================
setFullSluggedValue($this);
}
/**
* setFullSluggedValue will set the fullslug value on a model and recurse
* into children. For translatable models, the Translatable trait intercepts
* attribute access for the active locale automatically.
*/
protected function setFullSluggedValue($model)
{
$fullslugAttr = $this->getFullSluggableFullSlugColumnName();
$proposedSlug = $this->getFullSluggableAttributeValue($model);
if ($model->{$fullslugAttr} !== $proposedSlug) {
$model->{$fullslugAttr} = $proposedSlug;
$model->saveQuietly(['force' => true]);
}
if ($children = $model->children) {
foreach ($children as $child) {
$this->setFullSluggedValue($child);
}
}
}
/**
* getFullSluggableAttributeValue builds the fullslug by walking up the
* parent chain using the model's slug attribute
*/
protected function getFullSluggableAttributeValue($model, $fullslug = '')
{
$slugAttr = $this->getFullSluggableSlugColumnName();
$fullslug = $model->{$slugAttr} . '/' . $fullslug;
if ($parent = $model->parent()->withoutGlobalScopes()->first()) {
$fullslug = $this->getFullSluggableAttributeValue($parent, $fullslug);
}
return rtrim($fullslug, '/');
}
/**
* getFullSluggableFullSlugColumnName gets the name of the "fullslug" column.
* @return string
*/
public function getFullSluggableFullSlugColumnName()
{
return defined('static::FULLSLUG') ? static::FULLSLUG : 'fullslug';
}
/**
* getFullSluggableSlugColumnName gets the name of the "slug" column.
* @return string
*/
public function getFullSluggableSlugColumnName()
{
return defined('static::SLUG') ? static::SLUG : 'slug';
}
}
================================================
FILE: src/Database/Traits/SoftDelete.php
================================================
bindEvent('model.afterTrash', function() use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} has been trashed!");
* });
*
*/
$model->fireEvent('model.afterTrash');
if ($model->methodExists('afterTrash')) {
$model->afterTrash();
}
});
static::restoring(function($model) {
/**
* @event model.beforeRestore
* Called before the model is restored from a soft delete
*
* Example usage:
*
* $model->bindEvent('model.beforeRestore', function() use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} is going to be restored!");
* });
*
*/
$model->fireEvent('model.beforeRestore');
if ($model->methodExists('beforeRestore')) {
$model->beforeRestore();
}
});
static::restored(function($model) {
/**
* @event model.afterRestore
* Called after the model is restored from a soft delete
*
* Example usage:
*
* $model->bindEvent('model.afterRestore', function() use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} has been brought back to life!");
* });
*
*/
$model->fireEvent('model.afterRestore');
if ($model->methodExists('afterRestore')) {
$model->afterRestore();
}
});
}
/**
* isSoftDelete helper method to check if the model is currently
* being hard or soft deleted, useful in events.
* @return bool
*/
public function isSoftDelete()
{
return !$this->forceDeleting;
}
/**
* forceDelete on a soft deleted model.
*/
public function forceDelete()
{
$this->forceDeleting = true;
$this->delete();
$this->forceDeleting = false;
}
/**
* performDeleteOnModel performs the actual delete query on this model instance.
*/
protected function performDeleteOnModel()
{
if ($this->forceDeleting || !$this->isSoftDeleteEnabled()) {
$this->performDeleteOnRelations();
$this->setKeysForSaveQuery($this->newQuery()->withTrashed())->forceDelete();
$this->exists = false;
}
else {
$this->performSoftDeleteOnRelations();
$this->runSoftDelete();
}
}
/**
* performSoftDeleteOnRelations locates relations with softDelete flag and
* cascades the delete event.
*/
protected function performSoftDeleteOnRelations()
{
$definitions = $this->getRelationDefinitions();
foreach ($definitions as $type => $relations) {
foreach ($relations as $name => $options) {
if (!Arr::get($options, 'softDelete', false)) {
continue;
}
if (!$relation = $this->{$name}) {
continue;
}
if ($relation instanceof EloquentModel) {
$relation->delete();
}
elseif ($relation instanceof CollectionBase) {
$relation->each(function ($model) {
$model->delete();
});
}
}
}
}
/**
* runSoftDelete performs the actual delete query on this model instance.
*/
protected function runSoftDelete()
{
$query = $this->setKeysForSaveQuery($this->newQuery());
$time = $this->freshTimestamp();
$columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];
$this->{$this->getDeletedAtColumn()} = $time;
if ($this->timestamps && ! is_null($this->getUpdatedAtColumn())) {
$this->{$this->getUpdatedAtColumn()} = $time;
$columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
}
$query->update($columns);
$this->syncOriginalAttributes(array_keys($columns));
$this->fireModelEvent('trashed', false);
}
/**
* restore a soft-deleted model instance.
* @return bool|null
*/
public function restore()
{
// If the restoring event does not return false, we will proceed with this
// restore operation. Otherwise, we bail out so the developer will stop
// the restore totally. We will clear the deleted timestamp and save.
if ($this->fireModelEvent('restoring') === false) {
return false;
}
$this->performRestoreOnRelations();
$this->{$this->getDeletedAtColumn()} = null;
// Once we have saved the model, we will fire the "restored" event so this
// developer will do anything they need to after a restore operation is
// totally finished. Then we will return the result of the save call.
$this->exists = true;
$result = $this->save();
$this->fireModelEvent('restored', false);
return $result;
}
/**
* performRestoreOnRelations locates relations with softDelete flag and cascades
* the restore event.
*/
protected function performRestoreOnRelations()
{
$definitions = $this->getRelationDefinitions();
foreach ($definitions as $type => $relations) {
foreach ($relations as $name => $options) {
if (!Arr::get($options, 'softDelete', false)) {
continue;
}
$relation = $this->{$name}()->onlyTrashed()->getResults();
if (!$relation) {
continue;
}
if ($relation instanceof EloquentModel) {
$relation->restore();
}
elseif ($relation instanceof CollectionBase) {
$relation->each(function ($model) {
$model->restore();
});
}
}
}
}
/**
* trashed determines if the model instance has been soft-deleted.
* @return bool
*/
public function trashed()
{
return !is_null($this->{$this->getDeletedAtColumn()});
}
/**
* withTrashed gets a new query builder that includes soft deletes.
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public static function withTrashed()
{
return with(new static)->newQueryWithoutScope(new SoftDeleteScope);
}
/**
* onlyTrashed gets a new query builder that only includes soft deletes.
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public static function onlyTrashed()
{
$instance = new static;
$column = $instance->getQualifiedDeletedAtColumn();
return $instance->newQueryWithoutScope(new SoftDeleteScope)->whereNotNull($column);
}
/**
* softDeleted registers a "trashed" model event callback with the dispatcher.
* @param \Closure|string $callback
* @return void
*/
public static function softDeleted($callback)
{
static::registerModelEvent('trashed', $callback);
}
/**
* restoring registers a restoring model event with the dispatcher.
* @param \Closure|string $callback
* @return void
*/
public static function restoring($callback)
{
static::registerModelEvent('restoring', $callback);
}
/**
* restored registers a restored model event with the dispatcher.
* @param \Closure|string $callback
* @return void
*/
public static function restored($callback)
{
static::registerModelEvent('restored', $callback);
}
/**
* isSoftDeleteEnabled allows for programmatic toggling
* @return bool
*/
public function isSoftDeleteEnabled()
{
return true;
}
/**
* getDeletedAtColumn gets the name of the "deleted at" column.
* @return string
*/
public function getDeletedAtColumn()
{
return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at';
}
/**
* getQualifiedDeletedAtColumn gets the fully qualified "deleted at" column.
* @return string
*/
public function getQualifiedDeletedAtColumn()
{
return $this->qualifyColumn($this->getDeletedAtColumn());
}
}
================================================
FILE: src/Database/Traits/Sortable.php
================================================
setSortableOrder($recordIds, $recordOrders);
*
* You can change the sort field used by declaring:
*
* const SORT_ORDER = 'my_sort_order';
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait Sortable
{
/**
* bootSortable trait for this model.
*/
public static function bootSortable()
{
static::addGlobalScope(new SortableScope);
}
/**
* initializeSortable trait for this model.
*/
public function initializeSortable()
{
$this->bindEvent('model.afterCreate', function () {
$sortOrderColumn = $this->getSortOrderColumn();
if (is_null($this->$sortOrderColumn)) {
$this->setSortableOrder([$this->getKey()], [$this->getKey()]);
}
});
}
/**
* setSortableOrder sets the sort order of records to the specified orders, supplying
* a reference pool of sorted values. If reference pool is true, then an incrementing
* pool is used.
* @param mixed $itemIds
* @param array|null|bool $referencePool
* @return void
*/
public function setSortableOrder($itemIds, $referencePool = null)
{
if (!is_array($itemIds)) {
return;
}
$sortKeyMap = $this->processSortableOrdersInternal($itemIds, $referencePool);
if (count($itemIds) !== count($sortKeyMap)) {
throw new Exception('Invalid setSortableOrder call - count of itemIds do not match count of referencePool');
}
// Multisite
$keyName = $this->getKeyName();
if (
$this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$this->isMultisiteSyncEnabled() &&
$this->getMultisiteConfig('structure', true)
) {
$keyName = 'site_root_id';
}
$upsert = [];
foreach ($itemIds as $id) {
$sortOrder = $sortKeyMap[$id] ?? null;
if ($sortOrder !== null) {
$upsert[] = ['id' => $id, 'sort_order' => (int) $sortOrder];
}
}
if ($upsert) {
foreach ($upsert as $update) {
$this->newQuery()
->where($keyName, $update['id'])
->update([$this->getSortOrderColumn() => $update['sort_order']]);
}
}
$this->fireEvent('model.setSortableOrder');
}
/**
* processSortableOrdersInternal
*/
protected function processSortableOrdersInternal($itemIds, $referencePool = null): array
{
// Build incrementing reference pool
if ($referencePool === true) {
$referencePool = range(1, count($itemIds));
}
else {
// Extract a reference pool from the database
if (!$referencePool) {
$referencePool = $this->newQuery()
->whereIn($this->getKeyName(), $itemIds)
->pluck($this->getSortOrderColumn())
->all();
}
// Check for corrupt values, if found, reset with a unique pool
$referencePool = array_unique(array_filter($referencePool, 'strlen'));
if (count($referencePool) !== count($itemIds)) {
$referencePool = $itemIds;
}
// Sort pool to apply against the sorted items
sort($referencePool);
}
// Process the item orders to a sort key map
$result = [];
foreach ($itemIds as $index => $id) {
$result[$id] = $referencePool[$index];
}
return $result;
}
/**
* resetSortableOrdering can be used to repair corrupt or missing sortable definitions.
*/
public function resetSortableOrdering()
{
$ids = $this->newQuery()->pluck($this->getKeyName());
foreach ($ids as $id) {
$this->newQuery()->where($this->getKeyName(), $id)->update([$this->getSortOrderColumn() => $id]);
}
}
/**
* getSortOrderColumn name of the "sort order" column.
* @return string
*/
public function getSortOrderColumn()
{
return defined('static::SORT_ORDER') ? static::SORT_ORDER : 'sort_order';
}
/**
* getQualifiedSortOrderColumn gets the fully qualified "sort order" column.
* @return string
*/
public function getQualifiedSortOrderColumn()
{
return $this->qualifyColumn($this->getSortOrderColumn());
}
}
================================================
FILE: src/Database/Traits/SortableRelation.php
================================================
'sort_order_column'];
*
* To set orders:
*
* $model->setSortableRelationOrder($relationName, $recordIds, $recordOrders);
*
* @package october\database
* @author Alexey Bobkov, Samuel Georges
*/
trait SortableRelation
{
/**
* @var array sortableRelationDefinitions
*/
protected $sortableRelationDefinitions;
/**
* initializeSortableRelation trait for the model.
*/
public function initializeSortableRelation()
{
$this->bindEvent('model.afterInit', function() {
$this->defineSortableRelations();
});
$this->bindEvent('model.relation.attach', function ($relationName, $attached, $data) {
if (!array_key_exists($relationName, $this->getSortableRelations())) {
return;
}
// Order already set in pivot data (assuming singular)
$column = $this->getRelationSortOrderColumn($relationName);
if (is_array($data) && array_key_exists($column, $data)) {
return;
}
// Calculate a new order
$relation = $this->$relationName();
$order = $relation->max($relation->qualifyPivotColumn($column));
foreach ((array) $attached as $id) {
$relation->updateExistingPivot($id, [$column => ++$order]);
}
});
}
/**
* defineSortableRelations will spin over every relation and check for pivotSortable mode
*/
protected function defineSortableRelations()
{
$interactsWithPivot = ['belongsToMany', 'morphToMany'];
$sortableRelations = [];
foreach ($interactsWithPivot as $type) {
foreach ($this->$type as $name => $definition) {
if (!isset($definition['pivotSortable'])) {
continue;
}
$sortableRelations[$name] = $attrName = $definition['pivotSortable'];
// Ensure attribute is included in pivot definition
if (!isset($definition['pivot']) || !in_array($attrName, $definition['pivot'])) {
$this->$type[$name]['pivot'][] = $attrName;
}
// Apply sort by the pivot table column name
if (!isset($definition['order'])) {
$tableName = $definition['table'] ?? $this->$name()->getTable();
$this->$type[$name]['order'][] = $tableName.'.'.$attrName;
}
}
}
$this->sortableRelationDefinitions = $sortableRelations;
}
/**
* setSortableRelationOrder sets the sort order of records to the specified orders. If the orders is
* undefined, the record identifier is used. If reference pool is true, then an incrementing
* pool is used.
* @param string $relationName
* @param mixed $itemIds
* @param array|null|bool $referencePool
*/
public function setSortableRelationOrder($relationName, $itemIds, $referencePool = null)
{
if (!$this->isSortableRelation($relationName)) {
throw new Exception("Invalid setSortableRelationOrder call - the relation '{$relationName}' is not sortable");
}
if (!is_array($itemIds)) {
return;
}
$sortKeyMap = $this->processSortableRelationOrdersInternal($relationName, $itemIds, $referencePool);
if (count($itemIds) !== count($sortKeyMap)) {
throw new Exception('Invalid setSortableRelationOrder call - count of itemIds do not match count of itemOrders');
}
$upsert = [];
foreach ($itemIds as $id) {
$sortOrder = $sortKeyMap[$id] ?? null;
if ($sortOrder !== null) {
$upsert[] = ['id' => $id, 'sort_order' => (int) $sortOrder];
}
}
if ($upsert) {
foreach ($upsert as $update) {
$result = $this->exists ? $this->$relationName()->updateExistingPivot($update['id'], [
$this->getRelationSortOrderColumn($relationName) => $update['sort_order']
]) : 0;
if (!$result && $this->sessionKey) {
Db::table('deferred_bindings')
->where('master_field', $relationName)
->where('master_type', get_class($this))
->where('session_key', $this->sessionKey)
->where('slave_id', $update['id'])
->limit(1)
->update(['sort_order' => $update['sort_order']]);
}
}
}
}
/**
* processSortableRelationOrdersInternal
*/
protected function processSortableRelationOrdersInternal($relationName, $itemIds, $referencePool = null): array
{
// Build incrementing reference pool
if ($referencePool === true) {
$referencePool = range(1, count($itemIds));
}
else {
// Extract a reference pool from the database
if (!$referencePool) {
$referencePool = $this->$relationName()
->whereIn($this->getKeyName(), $itemIds)
->pluck($this->getRelationSortOrderColumn($relationName))
->all();
}
// Check for corrupt values, if found, reset with a unique pool
$referencePool = array_unique(array_filter($referencePool, 'strlen'));
if (count($referencePool) !== count($itemIds)) {
$referencePool = $itemIds;
}
// Sort pool to apply against the sorted items
sort($referencePool);
}
// Process the item orders to a sort key map
$result = [];
foreach ($itemIds as $index => $id) {
$result[$id] = $referencePool[$index];
}
return $result;
}
/**
* isSortableRelation returns true if the supplied relation is sortable.
*/
public function isSortableRelation($relationName)
{
return isset($this->sortableRelationDefinitions[$relationName]);
}
/**
* getRelationSortOrderColumn gets the name of the "sort_order" column.
*/
public function getRelationSortOrderColumn(string $relation): string
{
return $this->sortableRelationDefinitions[$relation] ?? 'sort_order';
}
/**
* getSortableRelations returns all configured sortable relations.
*/
protected function getSortableRelations(): array
{
return $this->sortableRelationDefinitions;
}
}
================================================
FILE: src/Database/Traits/Translatable.php
================================================
translatable)) {
throw new Exception(sprintf(
'The $translatable property in %s must be an array to use the Translatable trait.',
static::class
));
}
$this->morphMany['translations'] = [
$this->getTranslateAttributeModelClass(),
'name' => 'model',
'delete' => true
];
// Promote translated values into $attributes after fetch
$this->bindEvent('model.afterFetch', function() {
$this->promoteTranslatableValues();
});
// Demote + persist translations before save
$this->bindEvent('model.saveInternal', function() {
$this->syncTranslatableAttributes();
});
}
//
// Locale resolution
//
/**
* getTranslatableContext returns the active locale, resolved lazily from Site.
* Lazy resolution avoids issues during migrations/seeds when Site isn't booted.
*/
public function getTranslatableContext()
{
if ($this->translatableContext === null) {
$this->translatableContext = $this->resolveTranslatableLocale();
}
return $this->translatableContext;
}
/**
* getTranslatableDefault returns the default locale, resolved lazily from Site.
*/
public function getTranslatableDefault()
{
if ($this->translatableDefault === null) {
$this->translatableDefault = $this->resolveTranslatableDefaultLocale();
}
return $this->translatableDefault;
}
/**
* resolveTranslatableLocale reads the current locale from the Site facade.
* Override this to change how the active locale is determined.
*/
protected function resolveTranslatableLocale()
{
$site = Site::getSiteFromContext();
return $site ? $site->hard_locale : $this->getTranslatableDefault();
}
/**
* resolveTranslatableDefaultLocale reads the default locale from the Site facade.
* Override this to change how the default locale is determined.
*/
protected function resolveTranslatableDefaultLocale()
{
$site = Site::getPrimarySite();
return $site ? $site->hard_locale : 'en';
}
//
// Activation & bypass
//
/**
* isTranslatableEnabled returns true to indicate the trait is active
*/
public function isTranslatableEnabled()
{
return true;
}
/**
* shouldTranslate returns true when the active locale differs from the default.
* Returns false for single-locale installs so the trait is invisible.
*/
public function shouldTranslate()
{
if (!$this->isTranslatableEnabled()) {
return false;
}
return $this->getTranslatableContext() !== $this->getTranslatableDefault();
}
/**
* isTranslatableAttribute checks if a specific attribute should be translated right now.
* Returns false when: default locale active, or attribute not in $translatable.
*/
public function isTranslatableAttribute($key)
{
if ($key === 'translatable' || !$this->shouldTranslate()) {
return false;
}
return in_array($key, $this->getTranslatableAttributes());
}
/**
* getTranslatableAttributes returns the translatable attribute names
*/
public function getTranslatableAttributes()
{
return $this->translatable;
}
//
// Promote & demote
//
/**
* promoteTranslatableValues swaps translated values into $attributes
* and stashes the base (default-locale) values for later restoration.
* Called after fetch and when setLocale() changes context.
*/
protected function promoteTranslatableValues()
{
if (!$this->shouldTranslate()) {
// Default locale active: restore base values if previously promoted
if (!empty($this->translatableBaseValues)) {
$this->restoreTranslatableBaseValues();
}
return;
}
$locale = $this->getTranslatableContext();
$translatable = $this->getTranslatableAttributes();
// Stash current base values
foreach ($translatable as $key) {
if (array_key_exists($key, $this->attributes)) {
$this->translatableBaseValues[$key] = $this->attributes[$key];
}
}
// Load translations for this locale if not already loaded
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
// Promote: overwrite $attributes with translated values
$translated = $this->translatableAttributes[$locale] ?? [];
foreach ($translatable as $key) {
if (array_key_exists($key, $translated)) {
$this->attributes[$key] = $translated[$key];
}
}
}
/**
* demoteTranslatableValues reads current $attributes back into the
* translation cache and restores base (default-locale) values for the DB write
*/
protected function demoteTranslatableValues()
{
if (!$this->shouldTranslate() || empty($this->translatableBaseValues)) {
return;
}
$locale = $this->getTranslatableContext();
$translatable = $this->getTranslatableAttributes();
// Read current (possibly modified) translated values back from $attributes
foreach ($translatable as $key) {
if (array_key_exists($key, $this->attributes)) {
$this->translatableAttributes[$locale][$key] = $this->attributes[$key];
}
}
// Restore base values to $attributes for the DB write
$this->restoreTranslatableBaseValues();
}
/**
* restoreTranslatableBaseValues restores the stashed default-locale values
* back into $attributes
*/
protected function restoreTranslatableBaseValues()
{
foreach ($this->translatableBaseValues as $key => $value) {
$this->attributes[$key] = $value;
}
$this->translatableBaseValues = [];
}
//
// Base value access
//
/**
* getTranslatableBaseValue returns the default-locale value for a translatable
* attribute. When a non-default locale is promoted, reads from the stash.
* When the default locale is active, reads from $attributes directly.
*/
public function getTranslatableBaseValue(string $key)
{
if (!empty($this->translatableBaseValues) && array_key_exists($key, $this->translatableBaseValues)) {
return $this->translatableBaseValues[$key];
}
return $this->attributes[$key] ?? null;
}
//
// Reading translations
//
/**
* getTranslation returns the translated value for an attribute and locale
*/
public function getTranslation($key, $locale, $useFallback = true)
{
// Active promoted locale: read from $attributes
if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {
$result = $this->attributes[$key] ?? null;
}
// Default locale: read from base values
elseif ($locale === $this->getTranslatableDefault()) {
$result = $this->getTranslatableBaseValue($key);
}
// Other locale: read from sidecar cache
else {
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
if ($this->hasTranslation($key, $locale)) {
$result = $this->translatableAttributes[$locale][$key] ?? null;
}
elseif ($useFallback) {
$result = $this->getTranslatableBaseValue($key);
}
else {
$result = null;
}
}
// Handle jsonable attributes
if (
is_string($result) &&
method_exists($this, 'isJsonable') &&
$this->isJsonable($key)
) {
$result = json_decode($result, true);
}
return $result;
}
/**
* getTranslations returns all locale values for a single attribute
*/
public function getTranslations($key)
{
$translations = [];
// Default locale from base values
$defaultLocale = $this->getTranslatableDefault();
$defaultValue = $this->getTranslatableBaseValue($key);
if ($defaultValue !== null) {
$translations[$defaultLocale] = $defaultValue;
}
// Other locales from translation table
$rows = $this->translations->where('attribute', $key);
foreach ($rows as $row) {
$translations[$row->locale] = $row->value;
}
// Handle jsonable attributes
if (method_exists($this, 'isJsonable') && $this->isJsonable($key)) {
foreach ($translations as $locale => $value) {
if (is_string($value)) {
$translations[$locale] = json_decode($value, true);
}
}
}
return $translations;
}
/**
* hasTranslation checks if a translation row exists for one attribute
*/
public function hasTranslation($key, $locale = null)
{
if ($locale === null) {
$locale = $this->getTranslatableContext();
}
// Active promoted locale: check $attributes
if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {
$value = $this->attributes[$key] ?? null;
return $value !== null && $value !== '';
}
// Default locale: check base values
if ($locale === $this->getTranslatableDefault()) {
$value = $this->getTranslatableBaseValue($key);
return $value !== null && $value !== '';
}
// Other locale: check sidecar
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
$value = $this->translatableAttributes[$locale][$key] ?? null;
return $value !== null && $value !== '';
}
/**
* hasTranslations checks if any translation rows exist for the locale (record-level)
*/
public function hasTranslations($locale = null)
{
if ($locale === null) {
$locale = $this->getTranslatableContext();
}
if ($this->relationLoaded('translations')) {
return $this->translations->where('locale', $locale)->isNotEmpty();
}
return Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey())
->where('locale', $locale)
->exists();
}
/**
* getTranslatedLocales returns locales with translations. When a key is provided,
* returns locales for that specific attribute. Without a key, returns all locales
* that have any translation rows (record-level).
*/
public function getTranslatedLocales($key = null)
{
if ($this->relationLoaded('translations')) {
$query = $this->translations;
if ($key !== null) {
$query = $query->where('attribute', $key);
}
return $query->pluck('locale')->unique()->values()->toArray();
}
$query = Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey());
if ($key !== null) {
$query->where('attribute', $key);
}
return $query->pluck('locale')->unique()->toArray();
}
//
// Writing translations
//
/**
* setTranslation sets a translated value for an attribute and locale
*/
public function setTranslation($key, $locale, $value)
{
// Writing to the active promoted locale: write to $attributes directly
if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {
$this->attributes[$key] = $value;
return $value;
}
// Writing to the default locale: write to base values
if ($locale === $this->getTranslatableDefault()) {
if (!empty($this->translatableBaseValues)) {
$this->translatableBaseValues[$key] = $value;
}
else {
$this->attributes[$key] = $value;
}
return $value;
}
// Writing to a different non-active locale: write to sidecar cache
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
$this->translatableAttributes[$locale][$key] = $value;
return $value;
}
/**
* setTranslations sets multiple locale values at once for a single attribute
*/
public function setTranslations($key, array $translations)
{
foreach ($translations as $locale => $value) {
$this->setTranslation($key, $locale, $value);
}
}
//
// Deleting translations
//
/**
* forgetTranslation deletes a single translation row
*/
public function forgetTranslation($key, $locale)
{
Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey())
->where('locale', $locale)
->where('attribute', $key)
->delete();
unset($this->translatableAttributes[$locale][$key]);
unset($this->translatableOriginals[$locale][$key]);
}
/**
* forgetTranslations deletes all translation rows for an attribute (all locales)
*/
public function forgetTranslations($key)
{
Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey())
->where('attribute', $key)
->delete();
foreach ($this->translatableAttributes as $locale => &$data) {
unset($data[$key]);
}
foreach ($this->translatableOriginals as $locale => &$data) {
unset($data[$key]);
}
}
/**
* forgetAllTranslations deletes all translation rows for a locale ("unpublish French")
*/
public function forgetAllTranslations($locale)
{
Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey())
->where('locale', $locale)
->delete();
unset($this->translatableAttributes[$locale]);
unset($this->translatableOriginals[$locale]);
}
//
// Locale context
//
/**
* setLocale overrides the locale context for this model instance
*/
public function setLocale($locale)
{
// Demote current promoted values back to sidecar
if ($this->shouldTranslate() && !empty($this->translatableBaseValues)) {
$this->demoteTranslatableValues();
}
$this->translatableContext = $locale;
// Re-promote with new locale
if ($this->exists) {
$this->promoteTranslatableValues();
}
$this->fireEvent('model.translate.contextChange', [$locale]);
return $this;
}
/**
* getLocale returns the active locale (context override or site locale)
*/
public function getLocale()
{
return $this->getTranslatableContext();
}
//
// Dirty checking
//
/**
* isTranslateDirty determines if the model or a given translated attribute
* has been modified for a locale
*/
public function isTranslateDirty($attribute = null, $locale = null)
{
$dirty = $this->getTranslateDirty($locale);
if (is_null($attribute)) {
return count($dirty) > 0;
}
return array_key_exists($attribute, $dirty);
}
/**
* getTranslateDirty returns the translated attributes that have been changed
* since last sync
*/
public function getTranslateDirty($locale = null)
{
if (!$locale) {
$locale = $this->getTranslatableContext();
}
if (!array_key_exists($locale, $this->translatableAttributes)) {
return [];
}
// All dirty when no originals recorded
if (!array_key_exists($locale, $this->translatableOriginals)) {
return $this->translatableAttributes[$locale];
}
$dirty = [];
foreach ($this->translatableAttributes[$locale] as $key => $value) {
if (!array_key_exists($key, $this->translatableOriginals[$locale])) {
$dirty[$key] = $value;
}
elseif ($value != $this->translatableOriginals[$locale][$key]) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* getTranslatableOriginals gets the original values of the translated attributes
*/
public function getTranslatableOriginals($locale = null)
{
if (!$locale) {
return $this->translatableOriginals;
}
return $this->translatableOriginals[$locale] ?? null;
}
//
// Data storage
//
/**
* syncTranslatableAttributes demotes translated values and stores them
* in the translation table before the base model save
*/
protected function syncTranslatableAttributes()
{
// Demote: restore base values before save
$this->demoteTranslatableValues();
// Store translations for each known locale. When the model has no key
// yet (new record), defer until after insert assigns the primary key
if ($this->getKey()) {
$this->storeTranslatableBasicData();
}
else {
$this->bindEventOnce('model.saveComplete', function() {
$this->storeTranslatableBasicData();
});
}
}
/**
* storeTranslatableBasicData stores translations for each known dirty locale
*/
protected function storeTranslatableBasicData()
{
$knownLocales = array_keys($this->translatableAttributes);
foreach ($knownLocales as $locale) {
if (!$this->isTranslateDirty(null, $locale)) {
continue;
}
$this->fireEvent('model.translate.beforeSave', [$locale]);
$this->storeTranslatableData($locale);
$this->fireEvent('model.translate.afterSave', [$locale]);
}
}
/**
* storeTranslatableData saves translation data for a single locale using upsert
*/
protected function storeTranslatableData($locale)
{
$dirty = $this->getTranslateDirty($locale);
if (empty($dirty)) {
return;
}
$isDefaultLocale = ($locale === $this->getTranslatableDefault());
$rows = [];
foreach ($dirty as $key => $value) {
// For non-default locales, skip attributes whose value matches the
// model's local attribute (the default locale value). No row = inherits
// from default, so changes to the default automatically propagate.
if (!$isDefaultLocale) {
$defaultValue = $this->getTranslatableBaseValue($key);
if ($value === $defaultValue) {
continue;
}
}
// Serialize array values for storage
$storeValue = is_array($value) ? json_encode($value) : $value;
$rows[] = [
'model_type' => $this->getMorphClass(),
'model_id' => $this->getKey(),
'locale' => $locale,
'attribute' => $key,
'value' => $storeValue,
];
}
if (empty($rows)) {
return;
}
Db::table($this->getTranslateAttributeTable())->upsert(
$rows,
['model_type', 'model_id', 'locale', 'attribute'],
['value']
);
}
/**
* loadTranslatableData loads translation data for a locale, using the eager-loaded
* relationship when available, falling back to a direct query
*/
protected function loadTranslatableData($locale)
{
if ($this->relationLoaded('translations')) {
$rows = $this->translations
->where('locale', $locale)
->pluck('value', 'attribute')
->toArray();
}
else {
$rows = Db::table($this->getTranslateAttributeTable())
->where('model_type', $this->getMorphClass())
->where('model_id', $this->getKey())
->where('locale', $locale)
->pluck('value', 'attribute')
->toArray();
}
$this->translatableAttributes[$locale] = $rows;
$this->translatableOriginals[$locale] = $rows;
}
//
// Query scopes
//
/**
* scopeWhereTranslation adds a where clause for a translated attribute
*/
public function scopeWhereTranslation($query, $key, $locale, $value, $operator = '=')
{
return $query->whereExists(function ($q) use ($key, $locale, $value, $operator) {
$table = $this->getTranslateAttributeTable();
$q->select(Db::raw(1))
->from($table)
->whereColumn($table . '.model_id', $this->getQualifiedKeyName())
->where($table . '.model_type', $this->getMorphClass())
->where($table . '.locale', $locale)
->where($table . '.attribute', $key)
->where($table . '.value', $operator, $value);
});
}
/**
* scopeOrderByTranslation adds an order by clause for a translated attribute
*/
public function scopeOrderByTranslation($query, $key, $locale, $direction = 'asc')
{
$table = $this->getTranslateAttributeTable();
$alias = 'translate_order_' . $key;
return $query
->leftJoin($table . ' as ' . $alias, function ($join) use ($alias, $key, $locale) {
$join->on($alias . '.model_id', '=', $this->getQualifiedKeyName())
->where($alias . '.model_type', '=', $this->getMorphClass())
->where($alias . '.locale', '=', $locale)
->where($alias . '.attribute', '=', $key);
})
->orderBy($alias . '.value', $direction);
}
/**
* scopeWithTranslation eager loads translations for a single locale
*/
public function scopeWithTranslation($query, $locale = null)
{
if ($locale === null) {
$locale = $this->getTranslatableContext();
}
return $query->with(['translations' => function ($q) use ($locale) {
$q->where('locale', $locale);
}]);
}
/**
* scopeWithTranslations eager loads all translations (all locales)
*/
public function scopeWithTranslations($query)
{
return $query->with('translations');
}
//
// Helpers
//
/**
* getTranslateAttributeModelClass returns the model class used for translations.
* Resolved via the 'translate.attribute' container binding when available,
* falling back to the base TranslateAttribute model. Override per-model
* to use a custom translation table.
*/
public function getTranslateAttributeModelClass()
{
if (App::bound('core.translate.attribute')) {
return App::make('core.translate.attribute');
}
return \October\Rain\Database\Models\TranslateAttribute::class;
}
/**
* getTranslateAttributeTable returns the table name for translation storage
*/
public function getTranslateAttributeTable()
{
$modelClass = $this->getTranslateAttributeModelClass();
return (new $modelClass)->getTable();
}
}
================================================
FILE: src/Database/Traits/UserFootprints.php
================================================
bindEvent('model.saveInternal', function () {
$this->updateUserFootprints();
});
$userModel = $this->getUserFootprintAuth()->getProvider()->getModel();
$this->belongsTo['updated_user'] = [
$userModel,
'replicate' => false
];
$this->belongsTo['created_user'] = [
$userModel,
'replicate' => false
];
}
/**
* updateUserFootprints
*/
public function updateUserFootprints()
{
$userId = $this->getUserFootprintAuth()->id();
if (!$userId) {
return;
}
$updatedColumn = $this->getUpdatedUserIdColumn();
if ($updatedColumn !== null && !$this->isDirty($updatedColumn)) {
$this->{$updatedColumn} = $userId;
}
$createdColumn = $this->getCreatedUserIdColumn();
if (!$this->exists && $createdColumn !== null && !$this->isDirty($createdColumn)) {
$this->{$createdColumn} = $userId;
}
}
/**
* getCreatedUserIdColumn gets the name of the "created user id" column.
* @return string
*/
public function getCreatedUserIdColumn()
{
return defined('static::CREATED_USER_ID') ? static::CREATED_USER_ID : 'created_user_id';
}
/**
* getCreatedUserIdColumn gets the name of the "updated user id" column.
* @return string
*/
public function getUpdatedUserIdColumn()
{
return defined('static::UPDATED_USER_ID') ? static::UPDATED_USER_ID : 'updated_user_id';
}
/**
* getUserFootprintAuth
*/
protected function getUserFootprintAuth()
{
return App::make('backend.auth');
}
}
================================================
FILE: src/Database/Traits/Validation.php
================================================
rules)) {
throw new Exception(sprintf(
'The $rules property in %s must be an array to use the Validation trait.',
static::class
));
}
$this->bindEvent('model.saveInternal', function() {
$validationForced = $this->validationForced;
if (($forceOption = $this->getSaveOption('force')) !== null) {
$this->validationForced = $forceOption;
}
// If forcing the save event, the beforeValidate/afterValidate
// events should still fire for consistency. So validate an
// empty set of rules and messages.
if ($this->validationForced) {
$valid = $this->validate([], []);
}
else {
$valid = $this->validate();
}
$this->validationForced = $validationForced;
if (!$valid) {
return false;
}
}, 500);
}
/**
* setValidationAttributeNames programmatically sets multiple validation attribute names
* @param array $attributeNames
*/
public function setValidationAttributeNames($attributeNames)
{
$this->validationDefaultAttrNames = $attributeNames;
}
/**
* setValidationAttributeName programmatically sets the validation attribute names, will take
* lower priority to model defined attribute names found in `$attributeNames`
* @param string $attr
* @param string $name
* @return void
*/
public function setValidationAttributeName($attr, $name)
{
$this->validationDefaultAttrNames[$attr] = $name;
}
/**
* getValidationAttributes returns the model data used for validation
* @return array
*/
protected function getValidationAttributes()
{
return $this->getAttributes();
}
/**
* addValidationRule will append a rule to the stack and reset the value as a processed array
*/
public function addValidationRule(string $name, $definition)
{
$rules = $this->rules[$name] ?? [];
if (!is_array($rules)) {
$rules = explode('|', $rules);
}
if (is_array($definition)) {
$rules = array_merge($rules, $definition);
}
else {
$rules[] = $definition;
}
$this->rules[$name] = $rules;
}
/**
* removeValidationRule removes a validation rule from the stack and resets the value as a processed array
*/
public function removeValidationRule(string $name, $definition = '*')
{
if ($definition === '*') {
unset($this->rules[$name]);
return;
}
$rules = $this->rules[$name] ?? [];
if (!is_array($rules)) {
$rules = explode('|', $rules);
}
foreach ($rules as $key => $rule) {
if ($rule === $definition) {
unset($rules[$key]);
}
elseif (
is_string($definition) &&
is_string($rule) &&
str_starts_with($rule, "{$definition}:")
) {
unset($rules[$key]);
}
}
$this->rules[$name] = $rules;
}
/**
* getRelationValidationValue handles attachments that validate differently to their simple values
*/
protected function getRelationValidationValue($relationName)
{
// Locate records, with deferred logic
if (
$this->sessionKey &&
!$this->relationLoaded($relationName) &&
$this->hasDeferred($this->sessionKey, $relationName)
) {
$data = $this->$relationName()->withDeferred($this->sessionKey)->get();
}
else {
$data = $this->$relationName;
}
// DRY logic to post-process validation data
$processValidationValue = function($value) {
// Attachments
if ($value instanceof \October\Rain\Database\Attach\File) {
$localPath = $value->getLocalPath();
// Exception handling for UploadedFile
if (file_exists($localPath)) {
return new \Symfony\Component\HttpFoundation\File\UploadedFile(
$localPath,
$value->file_name,
$value->content_type,
null,
true
);
}
// Fallback to string
$value = $localPath;
}
return $value;
};
// Process singular
if ($this->isRelationTypeSingular($relationName)) {
if ($data instanceof \Illuminate\Support\Collection) {
$data = $data->last();
}
return $processValidationValue($data);
}
// Cast to primitive type
if ($data instanceof \Illuminate\Support\Collection) {
$data = $data->all();
}
if (!$data || !is_array($data)) {
return null;
}
// Process multi
$result = [];
foreach ($data as $key => $value) {
$result[$key] = $processValidationValue($value);
}
return $result;
}
/**
* makeValidator instantiates the validator used by the validation process, depending if the
* class is being used inside or outside of Laravel. Optional connection string to make
* the validator use a different database connection than the default connection.
* @return \Illuminate\Validation\Validator
*/
protected static function makeValidator($data, $rules, $customMessages, $attributeNames, $connection = null, $verifier = null)
{
// @deprecated make required arg (v4) desired signature below
// makeValidator($data, $rules, $customMessages, $attributeNames, $verifier)
//
if ($verifier === null) {
$verifier = App::make('validation.presence');
}
// @deprecated set via getValidationPresenceVerifier (v4)
if ($connection !== null) {
$verifier->setConnection($connection);
}
$validator = Validator::make($data, $rules, $customMessages, $attributeNames);
$validator->setPresenceVerifier($verifier);
return $validator;
}
/**
* getValidationPresenceVerifier
*/
protected function getValidationPresenceVerifier()
{
$verifier = App::make('validation.presence');
$verifier->setConnection($this->getConnectionName());
return $verifier;
}
/**
* forceSave the model even if validation fails
* @return bool
*/
public function forceSave($options = null, $sessionKey = null)
{
return $this->saveInternal((array) $options + ['force' => true, 'sessionKey' => $sessionKey]);
}
/**
* validate the model instance
* @return bool
*/
public function validate($rules = null, $customMessages = null, $attributeNames = null)
{
if ($this->validationErrors === null) {
$this->validationErrors = new MessageBag;
}
$throwOnValidation = property_exists($this, 'throwOnValidation')
? $this->throwOnValidation
: true;
/**
* @event model.beforeValidate
* Called before the model is validated
*
* Example usage:
*
* $model->bindEvent('model.beforeValidate', function () use (\October\Rain\Database\Model $model) {
* // Prevent anything from validating ever!
* return false;
* });
*
*/
if (($this->fireModelEvent('validating') === false) || ($this->fireEvent('model.beforeValidate', [], true) === false)) {
if ($throwOnValidation) {
throw new ModelException($this);
}
return false;
}
if ($this->methodExists('beforeValidate')) {
$this->beforeValidate();
}
// Perform validation
$rules = is_null($rules) ? $this->rules : $rules;
$rules = $this->processValidationRules($rules);
$success = true;
if (!empty($rules)) {
$data = $this->getValidationAttributes();
// Decode jsonable attribute values
foreach ($this->getJsonable() as $jsonable) {
$data[$jsonable] = $this->getAttribute($jsonable);
}
// Add relation values, if specified.
foreach ($rules as $attribute => $rule) {
if (!$this->hasRelation($attribute) || array_key_exists($attribute, $data)) {
continue;
}
$data[$attribute] = $this->getRelationValidationValue($attribute);
}
// Compatibility with Hashable trait
// Remove all hashable values regardless, add the original values back
// only if they are part of the data being validated.
if (method_exists($this, 'getHashableAttributes')) {
$cleanAttributes = array_diff_key($data, array_flip($this->getHashableAttributes()));
$hashedAttributes = array_intersect_key($this->getOriginalHashValues(), $data);
$data = array_merge($cleanAttributes, $hashedAttributes);
}
// Compatibility with Encryptable trait
// Remove all encryptable values regardless, add the original values back
// only if they are part of the data being validated.
if (method_exists($this, 'getEncryptableAttributes')) {
$cleanAttributes = array_diff_key($data, array_flip($this->getEncryptableAttributes()));
$encryptedAttributes = array_intersect_key($this->getOriginalEncryptableValues(), $data);
$data = array_merge($cleanAttributes, $encryptedAttributes);
}
// Custom messages, translate internal references
if (property_exists($this, 'customMessages') && is_null($customMessages)) {
$customMessages = $this->customMessages;
}
if (is_null($customMessages)) {
$customMessages = [];
}
$transCustomMessages = [];
foreach ($customMessages as $rule => $customMessage) {
$transCustomMessages[$rule] = Lang::get($customMessage);
}
$customMessages = $transCustomMessages;
// Attribute names, translate internal references
$attrNames = (array) $this->validationDefaultAttrNames;
if (property_exists($this, 'attributeNames')) {
$attrNames = array_merge($attrNames, $this->attributeNames);
}
if ($attributeNames) {
$attrNames = array_merge($attrNames, (array) $attributeNames);
}
$transAttrNames = [];
foreach ($attrNames as $attribute => $attributeName) {
$transAttrNames[$attribute] = Lang::get($attributeName);
}
$attrNames = $transAttrNames;
// Translate any externally defined attribute names
$translations = Lang::get('validation.attributes');
if (is_array($translations)) {
$attrNames = array_merge($translations, $attrNames);
}
// Hand over to the validator
$validator = self::makeValidator(
$data,
$rules,
$customMessages,
$attrNames,
$this->getConnectionName(),
$this->getValidationPresenceVerifier()
);
$success = $validator->passes();
if ($success) {
if ($this->validationErrors->count() > 0) {
$this->validationErrors = new MessageBag;
}
}
else {
$this->validationErrors = $validator->messages();
if (Input::hasSession()) {
Input::flash();
}
}
}
/**
* @event model.afterValidate
* Called after the model is validated
*
* Example usage:
*
* $model->bindEvent('model.afterValidate', function () use (\October\Rain\Database\Model $model) {
* \Log::info("{$model->name} successfully passed validation");
* });
*
*/
$this->fireModelEvent('validated', false);
$this->fireEvent('model.afterValidate');
if ($this->methodExists('afterValidate')) {
$this->afterValidate();
}
if (!$success && $throwOnValidation) {
throw new ModelException($this);
}
return $success;
}
/**
* processValidationRules
*/
protected function processValidationRules($rules)
{
// Run through field names and convert array notation field names to dot notation
$rules = $this->processRuleFieldNames($rules);
foreach ($rules as $field => $ruleParts) {
// Trim empty rules
if (is_string($ruleParts) && trim($ruleParts) === '') {
unset($rules[$field]);
continue;
}
// Normalize rule sets
if (!is_array($ruleParts)) {
$ruleParts = explode('|', $ruleParts);
}
// Analyze each rule individually
foreach ($ruleParts as $key => $rulePart) {
// Allow rule objects
if (is_object($rulePart)) {
continue;
}
// Remove primary key unique validation rule if the model already exists
if (str_starts_with($rulePart, 'unique')) {
$ruleParts[$key] = $this->processValidationUniqueRule($rulePart, $field);
}
// Look for required:create and required:update rules
elseif (str_starts_with($rulePart, 'required:create') && $this->exists) {
unset($ruleParts[$key]);
}
elseif (str_starts_with($rulePart, 'required:update') && !$this->exists) {
unset($ruleParts[$key]);
}
}
$rules[$field] = $ruleParts;
}
return $rules;
}
/**
* processRuleFieldNames processes field names in a rule array
* Converts any field names using array notation (ie. `field[child]`) into dot notation (ie. `field.child`)
* @param array $rules Rules array
* @return array
*/
protected function processRuleFieldNames($rules)
{
$processed = [];
foreach ($rules as $field => $ruleParts) {
$fieldName = $field;
if (preg_match('/^.*?\[.*?\]/', $fieldName)) {
$fieldName = str_replace('[]', '.*', $fieldName);
$fieldName = str_replace(['[', ']'], ['.', ''], $fieldName);
}
$processed[$fieldName] = $ruleParts;
}
return $processed;
}
/**
* processValidationUniqueRule rebuilds the unique validation rule to force for the existing key
* exclusion for existing models. It also checks for unique rules without a table name and includes
* the table name, since this is required by Laravel but not October.
* @param string $definition
* @param string $fieldName
* @return string
*/
protected function processValidationUniqueRule($definition, $fieldName)
{
if (!$this->exists) {
if ($definition === 'unique' || $definition === 'unique_site') {
return $definition . ':' . $this->getTable();
}
return $definition;
}
[$ruleName, $ruleDefinition] = array_pad(explode(':', $definition, 2), 2, '');
[$tableName, $column, $key, $keyName, $whereColumn, $whereValue] = array_pad(explode(',', $ruleDefinition, 6), 6, null);
$tableName = $tableName ?: $this->getTable();
$column = $column ?: $fieldName;
$key = $keyName ? $this->$keyName : $this->getKey();
$keyName = $keyName ?: $this->getKeyName();
$params = [$tableName, $column, $key, $keyName];
if ($whereColumn) {
$params[] = $whereColumn;
}
if ($whereValue) {
$params[] = $whereValue;
}
return $ruleName . ':' . implode(',', $params);
}
/**
* isAttributeRequired determines if an attribute is required based on the validation rules.
* checkDependencies checks the attribute dependencies (for required_if & required_with rules).
* Note that it will only be checked up to the next level, if another dependent rule is found
* then it will just assume the field is required.
* @param string $attribute
* @param bool $checkDependencies
* @return bool
*/
public function isAttributeRequired($attribute, $checkDependencies = true)
{
if (!isset($this->rules[$attribute])) {
return false;
}
$ruleSet = $this->rules[$attribute];
if (is_array($ruleSet)) {
$ruleSet = implode('|', $ruleSet);
}
if (strpos($ruleSet, 'required:create') !== false && $this->exists) {
return false;
}
if (strpos($ruleSet, 'required:update') !== false && !$this->exists) {
return false;
}
if (strpos($ruleSet, 'required_with') !== false) {
if (!$checkDependencies) {
return true;
}
$requiredWith = substr($ruleSet, strpos($ruleSet, 'required_with') + 14);
if (strpos($requiredWith, '|') !== false) {
$requiredWith = substr($requiredWith, 0, strpos($requiredWith, '|'));
}
return $this->isAttributeRequired($requiredWith, false);
}
if (strpos($ruleSet, 'required_if') !== false) {
if (!$checkDependencies) {
return true;
}
$requiredIf = substr($ruleSet, strpos($ruleSet, 'required_if') + 12);
$requiredIf = substr($requiredIf, 0, strpos($requiredIf, ','));
return $this->isAttributeRequired($requiredIf, false);
}
return strpos($ruleSet, 'required') !== false;
}
/**
* errors gets validation error message collection for the Model
* @return \Illuminate\Support\MessageBag
*/
public function errors()
{
return $this->validationErrors;
}
/**
* validating creates a new native event for handling beforeValidate()
* @param Closure|string $callback
* @return void
*/
public static function validating($callback)
{
static::registerModelEvent('validating', $callback);
}
/**
* validated create a new native event for handling afterValidate()
* @param Closure|string $callback
* @return void
*/
public static function validated($callback)
{
static::registerModelEvent('validated', $callback);
}
}
================================================
FILE: src/Database/TreeCollection.php
================================================
toNested(); // Converts collection to an eager loaded one.
*
*/
class TreeCollection extends Collection
{
/**
* toNested converts a flat collection of nested set models to an set where
* children is eager loaded. removeOrphans removes nodes that exist without
* their parents.
* @param bool $removeOrphans
* @return \October\Rain\Database\Collection
*/
public function toNested($removeOrphans = true)
{
// Multisite
$keyMethod = 'getKey';
if (
($model = $this->first()) &&
$model->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) &&
$model->isAttributePropagatable('children') &&
$model->isAttributePropagatable('parent')
) {
$keyMethod = 'getMultisiteKey';
}
// Get dictionary
$collection = [];
foreach ($this as $item) {
$collection[$item->{$keyMethod}()] = $item;
}
// Set new collection for "children" relations
foreach ($collection as $key => $model) {
$model->setRelation('children', new Collection);
}
// Assign all child nodes to their parents
$nestedKeys = [];
foreach ($collection as $key => $model) {
if (!$parentKey = $model->getParentId()) {
continue;
}
if (array_key_exists($parentKey, $collection)) {
$collection[$parentKey]->children[] = $model;
$nestedKeys[] = $model->{$keyMethod}();
}
elseif ($removeOrphans) {
$nestedKeys[] = $model->{$keyMethod}();
}
}
// Remove processed nodes
foreach ($nestedKeys as $key) {
unset($collection[$key]);
}
return new Collection($collection);
}
/**
* listsNested gets an array with values of a given column. Values are indented according
* to their depth.
* @param string $value Array values
* @param string $key Array keys
* @param string $indent Character to indent depth
* @return array
*/
public function listsNested($value, $key = null, $indent = ' ')
{
// Recursive helper function
$buildCollection = function ($items, $depth = 0) use (&$buildCollection, $value, $key, $indent) {
$result = [];
$indentString = str_repeat($indent, $depth);
foreach ($items as $item) {
if ($key !== null) {
$result[$item->{$key}] = $indentString . $item->{$value};
}
else {
$result[] = $indentString . $item->{$value};
}
// Add the children
$childItems = $item->getChildren();
if ($childItems->count() > 0) {
$result = $result + $buildCollection($childItems, $depth + 1);
}
}
return $result;
};
// Build a nested collection
$rootItems = $this->toNested();
return $buildCollection($rootItems);
}
}
================================================
FILE: src/Database/Updater.php
================================================
resolve($file);
if ($object === null) {
return false;
}
$this->isValidScript($object);
Model::unguard();
if ($object instanceof Updates\Migration) {
$this->runMethod($object, 'up');
}
elseif ($object instanceof Updates\Seeder) {
$this->runMethod($object, 'run');
}
Model::reguard();
return true;
}
/**
* packDown a migration or seed file.
*/
public function packDown($file)
{
$object = $this->resolve($file);
if ($object === null) {
return false;
}
$this->isValidScript($object);
Model::unguard();
if ($object instanceof Updates\Migration) {
$this->runMethod($object, 'down');
}
Model::reguard();
return true;
}
/**
* resolve a migration instance from a file.
* @param string $file
* @return object
*/
public function resolve(string $path)
{
if (!is_file($path)) {
return;
}
$class = $this->getClassFromFile($path);
if (class_exists($class) && realpath($path) == (new ReflectionClass($class))->getFileName()) {
return new $class;
}
$migration = static::$requiredPathCache[$path] ??= require $path;
if (is_object($migration)) {
return method_exists($migration, '__construct')
? require $path
: clone $migration;
}
if (str_ends_with($class, 'class@anonymous')) {
throw new Exception("Anonymous class in [{$path}] could not be resolved");
}
return new $class;
}
/**
* runMethod on a migration or seed
*/
protected function runMethod($migration, $method)
{
try {
$migration->{$method}();
}
catch (Exception $ex) {
if (!static::$skippingErrors) {
throw $ex;
}
}
}
/**
* isValidScript checks if the object is a valid update script.
*/
protected function isValidScript($object)
{
if ($object instanceof Updates\Migration) {
return true;
}
elseif ($object instanceof Updates\Seeder) {
return true;
}
throw new Exception(sprintf(
'Database script [%s] must inherit October\Rain\Database\Updates\Migration or October\Rain\Database\Updates\Seeder classes',
get_class($object)
));
}
/**
* getClassFromFile extracts the namespace and class name from a file.
* @param string $file
* @return string
*/
public function getClassFromFile($file)
{
$fileParser = fopen($file, 'r');
$class = $namespace = $buffer = '';
$i = 0;
while (!$class) {
if (feof($fileParser)) {
break;
}
$buffer .= fread($fileParser, 512);
// Prefix and suffix string to prevent unterminated comment warning
$tokens = token_get_all('/**/' . $buffer . '/**/');
if (strpos($buffer, '{') === false) {
continue;
}
for (; $i < count($tokens); $i++) {
// Namespace opening
if ($tokens[$i][0] === T_NAMESPACE) {
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j] === ';') {
break;
}
$namespace .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];
}
}
// Class opening
if ($tokens[$i][0] === T_CLASS && $tokens[$i-1][1] !== '::') {
// Anonymous Class
if ($tokens[$i-2][0] === T_NEW && $tokens[$i-4][0] === T_RETURN) {
$class = 'class@anonymous';
break;
}
$class = $tokens[$i+2][1];
break;
}
}
}
if (!strlen(trim($namespace)) && !strlen(trim($class))) {
return false;
}
return trim($namespace) . '\\' . trim($class);
}
}
================================================
FILE: src/Database/Updates/Migration.php
================================================
command)) {
return;
}
$styled = $style ? "<$style>$string$style>" : $string;
$this->command->getOutput()->writeln($styled);
}
}
================================================
FILE: src/Element/Dash/ReportDefinition.php
================================================
displayAs('static')
->metrics([])
->width(20)
->row(1)
;
}
/**
* displayAs type for this field
*/
public function displayAs(string $type): ReportDefinition
{
$this->type($type);
return $this;
}
}
================================================
FILE: src/Element/ElementBase.php
================================================
initDefaultValues();
$this->useConfig($config);
}
/**
* initDefaultValues override method
*/
protected function initDefaultValues()
{
}
/**
* evalConfig override method
*/
public function evalConfig(array $config)
{
}
/**
* useConfig is used internally
*/
public function useConfig(array $config): ElementBase
{
$this->config = array_merge($this->config, $config);
$this->evalConfig($config);
return $this;
}
/**
* getConfig returns the entire config array
*/
public function getConfig($key = null, $default = null)
{
if ($key !== null) {
return $this->get($key, $default);
}
return $this->config;
}
/**
* get an attribute from the element instance.
* @param string $key
*/
public function get($key, $default = null)
{
if (array_key_exists($key, $this->config)) {
return $this->config[$key];
}
return value($default);
}
/**
* toArray converts the element instance to an array.
* @return array
*/
public function toArray()
{
return $this->config;
}
/**
* jsonSerialize converts the object into something JSON serializable.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* toJson converts the element instance to JSON.
* @param int $options
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->jsonSerialize(), $options);
}
/**
* offsetExists determines if the given offset exists.
* @param string $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->config[$offset]);
}
/**
* offsetGet gets the value for a given offset.
* @param string $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->get($offset);
}
/**
* offsetSet sets the value at the given offset.
* @param string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
$this->config[$offset] = $value;
}
/**
* offsetUnset unsets the value at the given offset.
* @param string $offset
* @return void
*/
public function offsetUnset($offset): void
{
unset($this->config[$offset]);
}
/**
* __call handles dynamic calls to the element instance to set config.
* @param string $method
* @param array $parameters
* @return $this
*/
public function __call($method, $parameters)
{
$this->config[$method] = count($parameters) > 0 ? $parameters[0] : true;
return $this;
}
/**
* __get dynamically retrieves the value of an attribute.
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
/**
* __set dynamically sets the value of an attribute.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function __set($key, $value)
{
$this->offsetSet($key, $value);
}
/**
* __isset dynamically checks if an attribute is set.
*
* @param string $key
* @return bool
*/
public function __isset($key)
{
return $this->offsetExists($key);
}
/**
* __unset dynamically unsets an attribute.
*
* @param string $key
* @return void
*/
public function __unset($key)
{
$this->offsetUnset($key);
}
}
================================================
FILE: src/Element/ElementHolder.php
================================================
touchedElements;
}
/**
* get an element from the holder instance.
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
if (isset($this->touchedElements[$key])) {
return $this->touchedElements[$key];
}
if (isset($this->config[$key])) {
return $this->touchedElements[$key] = $this->config[$key];
}
return parent::get($key, $default);
}
/**
* getIterator for the elements.
*/
public function getIterator(): Traversable
{
return new Collection($this->config);
}
}
================================================
FILE: src/Element/Filter/ScopeDefinition.php
================================================
displayAs('group')
->nameFrom('name')
->disabled(false)
->order(-1)
;
}
/**
* useConfig
*/
public function useConfig(array $config): ElementBase
{
parent::useConfig($config);
// The config default proxies to defaults
if (array_key_exists('default', $this->config)) {
$this->defaults($this->config['default']);
}
return $this;
}
/**
* displayAs type for this scope. Supported modes are:
* - group - filter by a group of IDs. Default.
* - checkbox - filter by a simple toggle switch.
*/
public function displayAs(string $type): ScopeDefinition
{
$this->type($type);
return $this;
}
/**
* hasOptions returns true if options have been specified
*/
public function hasOptions(): bool
{
return $this->options !== null &&
(is_array($this->options) || is_callable($this->options));
}
/**
* options get/set for dropdowns, radio lists and checkbox lists
* @return array|self
*/
public function options($value = null)
{
if ($value === null) {
if (is_array($this->options)) {
return $this->options;
}
if (is_callable($this->options)) {
$callable = $this->options;
return $callable();
}
return [];
}
$this->config['options'] = $value;
return $this;
}
/**
* optionsDefinition
*/
public function asOptionsDefinition($options = null)
{
if ($options === null) {
$options = $this->options();
}
$result = [];
foreach ($options as $value => $option) {
$result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);
}
return $result;
}
/**
* setScopeValue and merge the values as config
*/
public function setScopeValue($value)
{
if (is_array($value)) {
$this->config = array_merge($this->config, $value);
}
$this->scopeValue($value);
}
}
================================================
FILE: src/Element/Form/FieldDefinition.php
================================================
hidden(false)
->autoFocus(false)
->readOnly(false)
->disabled(false)
->displayAs('text')
->span('full')
->size('large')
->commentPosition('below')
->commentHtml(false)
->spanClass('')
->comment('')
->placeholder('')
->order(-1)
;
}
/**
* useConfig
*/
public function useConfig(array $config): ElementBase
{
parent::useConfig($config);
// The config default proxies to defaults
if (array_key_exists('default', $this->config)) {
$this->defaults($this->config['default']);
}
return $this;
}
/**
* displayAs type for this field
*/
public function displayAs(string $type): FieldDefinition
{
$this->type($type);
return $this;
}
/**
* span sets a side of the field on a form
*/
public function span(string $value = 'full', string $spanClass = ''): FieldDefinition
{
$this->span = $value;
$this->spanClass = $spanClass;
return $this;
}
/**
* hasOptions returns true if options have been specified
*/
public function hasOptions(): bool
{
if ($this->optionsCallback !== null) {
return true;
}
if ($this->options !== null && is_array($this->options)) {
return true;
}
return false;
}
/**
* options get/set for dropdowns, radio lists and checkbox lists
* @return array|self
*/
public function options($value = null)
{
// get
if ($value === null) {
if ($this->optionsCallback !== null) {
$callable = $this->optionsCallback;
return $callable();
}
if (is_array($this->options)) {
return $this->options;
}
return [];
}
// set
if (is_callable($value)) {
$this->optionsCallback = $value;
}
else {
$this->options = $value;
}
return $this;
}
/**
* optionsDefinition
*/
public function asOptionsDefinition($options = null)
{
if ($options === null) {
$options = $this->options();
}
$result = [];
foreach ($options as $value => $option) {
$result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);
}
return $result;
}
/**
* matchesContext returns true if the field matches the supplied context
*/
public function matchesContext($context): bool
{
if ($context === '*' || $this->context === null) {
return true;
}
return in_array($context, (array) $this->context);
}
}
================================================
FILE: src/Element/Form/FieldsetDefinition.php
================================================
defaultTab('Misc')
->suppressTabs(false)
;
}
/**
* addField to the collection of tabs
*/
public function addField($name, FieldDefinition $field)
{
$this->fields[$name] = $field;
}
/**
* removeField from all tabs by name
* @param string $name
* @return boolean
*/
public function removeField($name)
{
if (isset($this->fields[$name])) {
unset($this->fields[$name]);
return true;
}
return false;
}
/**
* hasFields returns true if any fields have been registered for these tabs
* @return bool
*/
public function hasFields()
{
return count($this->fields) > 0;
}
/**
* getFields returns an array of the registered fields, includes tabs in format
* array[tab][field]
* @return array
*/
public function getFields()
{
$fieldsTabbed = [];
foreach ($this->fields as $name => $field) {
$tabName = $field->tab ?: $this->defaultTab;
$fieldsTabbed[$tabName][$name] = $field;
}
return $fieldsTabbed;
}
/**
* getField object specified
*/
public function getField(string $field)
{
if (isset($this->fields[$field])) {
return $this->fields[$field];
}
return null;
}
/**
* getAllFields returns an array of the registered fields, without tabs
* @return array
*/
public function getAllFields()
{
return $this->fields;
}
/**
* sortAllFields will sort the defined fields by their order attribute
*/
public function sortAllFields()
{
uasort($this->fields, static function ($a, $b) {
return $a->order - $b->order;
});
}
/**
* getIterator gets an iterator for the items
* @return ArrayIterator
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator(
$this->suppressTabs
? $this->getAllFields()
: $this->getFields()
);
}
}
================================================
FILE: src/Element/Lists/ColumnDefinition.php
================================================
displayAs('text')
->hidden(false)
->sortable()
->searchable(false)
->invisible(false)
->clickable()
->order(-1)
;
}
/**
* displayAs type for this column
* @todo $config is deprecated, see useConfig
*/
public function displayAs(string $type): ColumnDefinition
{
$this->type = $type;
return $this;
}
}
================================================
FILE: src/Element/Navigation/ItemDefinition.php
================================================
order(-1)
;
}
}
================================================
FILE: src/Element/OptionDefinition.php
================================================
hidden(false)
->readOnly(false)
->disabled(false)
->comment('');
}
/**
* useOptionConfig
*/
public function useOptionConfig($value, $option): OptionDefinition
{
$this->value($value)->label($value);
// Option as string
if (!is_array($option)) {
$this->label($option);
return $this;
}
// Option as definition
if (Arr::isAssoc($option)) {
if (isset($option['children']) && is_array($option['children'])) {
$option['children'] = $this->evalChildOptions($option['children']);
}
$this->useConfig($option);
return $this;
}
// Option as [label, comment]
$firstPart = (string) ($option[0] ?? '');
$secondPart = (string) ($option[1] ?? '');
$this->label($firstPart);
$this->comment($secondPart);
if (Html::isValidColor($secondPart)) {
$this->color($secondPart);
}
elseif (strpos($secondPart, '.')) {
$this->image($secondPart);
}
else {
$this->icon($secondPart);
}
return $this;
}
/**
* evalChildOptions
*/
protected function evalChildOptions(array $children): array
{
$result = [];
foreach ($children as $value => $option) {
$result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);
}
return $result;
}
}
================================================
FILE: src/Events/Dispatcher.php
================================================
app->singleton('events', function ($app) {
// return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
// return $app->make(QueueFactoryContract::class);
// })->setTransactionManagerResolver(function () use ($app) {
// return $app->bound('db.transactions')
// ? $app->make('db.transactions')
// : null;
// });
// The following adds support for Laravel 10.30 when a transaction manager resolver
// was included as part of the dispatcher. Detect its presence and set it as needed
// @deprecated remove reflection and use code above in v4 (Laravel 11)
$dispatcher = (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
if (method_exists($dispatcher, 'setTransactionManagerResolver')) {
$dispatcher->setTransactionManagerResolver(function () use ($app) {
return $app->bound('db.transactions')
? $app->make('db.transactions')
: null;
});
}
return $dispatcher;
});
$this->app->singleton('events.priority', function ($app) {
return (new PriorityDispatcher($app))->setLaravelDispatcher($app['events']);
});
}
}
================================================
FILE: src/Events/FakeDispatcher.php
================================================
getLaravelDispatcher() : $dispatcher,
$eventsToFake
);
}
/**
* fire proxies to dispatch
*/
public function fire(...$args)
{
return parent::dispatch(...$args);
}
}
================================================
FILE: src/Events/PriorityDispatcher.php
================================================
container = $container ?: new Container;
}
/**
* listen registers an event listener with the dispatcher.
* @param string|array $events
* @param mixed|null $listener
* @param int $priority
* @return void
*/
public function listen($events, $listener = null, $priority = 0)
{
if ($priority === 0) {
$this->laravelEvents->listen($events, $listener);
}
else {
$this->bindEvent($events, $listener, $priority);
}
}
/**
* subscribe registers an event subscriber with the dispatcher, passing
* the PriorityDispatcher instance so priority arguments are respected.
* @param object|string $subscriber
* @return void
*/
public function subscribe($subscriber)
{
if (is_string($subscriber)) {
$subscriber = $this->container->make($subscriber);
}
$events = $subscriber->subscribe($this);
if (is_array($events)) {
foreach ($events as $event => $listeners) {
foreach ((array) $listeners as $listener) {
if (is_string($listener) && method_exists($subscriber, $listener)) {
$this->listen($event, [get_class($subscriber), $listener]);
continue;
}
$this->listen($event, $listener);
}
}
}
}
/**
* listenOnce registers an event that only fires once.
* @param string|array $events
* @param callable $listener
* @param int $priority
* @return void
*/
public function listenOnce($events, $listener, $priority = 0)
{
$this->bindEventOnce($events, $listener, $priority);
}
/**
* fire an event and call the listeners.
* @param string|object $event
* @param mixed $payload
* @param bool $halt
* @return array|null
*/
public function fire($event, $payload = [], $halt = false)
{
return $this->fireEvent($event, $payload, $halt);
}
/**
* forget removes a set of listeners from the dispatcher.
* @param string $event
* @return void
*/
public function forget($event)
{
$this->unbindEvent($event);
$this->laravelEvents->forget($event);
}
/**
* fireEvent inherits logic from the Emitter, modified to forward call to Laravel events
* @param string $event
* @param array $params
* @param boolean $halt
* @return array
*/
public function fireEvent($event, $params = [], $halt = false)
{
if (!is_array($params)) {
$params = [$params];
}
// Micro optimization
if (
!isset($this->emitterEventCollection[$event]) &&
!isset($this->emitterSingleEventCollection[$event])
) {
return $this->laravelEvents->dispatch($event, $params, $halt);
}
if (!isset($this->emitterEventSorted[$event])) {
$this->emitterEventSorted[$event] = $this->emitterEventSortEvents($event, [
0 => [self::FORWARD_CALL_FLAG]
]);
}
$result = [];
foreach ($this->emitterEventSorted[$event] as $callback) {
if ($callback === self::FORWARD_CALL_FLAG) {
$response = $this->laravelEvents->dispatch($event, $params, $halt);
$isLaravel = true;
}
else {
if (is_string($callback)) {
$callback = $this->createClassCallback($callback);
}
if (is_array($callback) && isset($callback[0]) && is_string($callback[0])) {
$callback = $this->createClassCallback($callback);
}
$response = $callback(...$params);
$isLaravel = false;
}
if (!is_null($response) && $halt) {
return $response;
}
if ($response === false) {
break;
}
if (!is_null($response)) {
if ($isLaravel) {
$result = array_merge($result, $response);
}
else {
$result[] = $response;
}
}
}
if (isset($this->emitterSingleEventCollection[$event])) {
unset($this->emitterSingleEventCollection[$event]);
unset($this->emitterEventSorted[$event]);
}
return $halt ? null : $result;
}
/**
* setLaravelDispatcher sets the event resolver implementation.
*/
public function setLaravelDispatcher(DispatcherContract $dispatcher): PriorityDispatcher
{
$this->laravelEvents = $dispatcher;
return $this;
}
/**
* getLaravelDispatcher returns the base event resolver.
*/
public function getLaravelDispatcher(): DispatcherContract
{
return $this->laravelEvents;
}
/**
* createClassCallback passes what is usually a static method call through the IoC
* container to create a callable instance.
*/
protected function createClassCallback($callback)
{
if (is_callable($callback)) {
return $callback;
}
[$class, $method] = is_array($callback)
? $callback
: Str::parseCallback($callback, 'handle');
if (!method_exists($class, $method)) {
$method = '__invoke';
}
$listener = $this->container->make($class);
return [$listener, $method];
}
/**
* __call magic
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->forwardCallTo(
$this->laravelEvents,
$method,
$parameters
);
}
}
================================================
FILE: src/Exception/AjaxException.php
================================================
$contents];
}
elseif (!is_array($contents)) {
$contents = [];
}
$this->contents = $contents;
parent::__construct(json_encode($contents));
}
/**
* getContents returns invalid fields.
*/
public function getContents()
{
return $this->contents;
}
/**
* addContent is used to add extra data to an AJAX exception
*/
public function addContent(string $key, $val)
{
$this->contents[$key] = $val;
}
/**
* toAjaxData
*/
public function toAjaxData(): array
{
return (array) $this->contents;
}
}
================================================
FILE: src/Exception/ApplicationException.php
================================================
getContents()
: static::getDetailedMessage($proposedException);
}
// Clear the output buffer
while (ob_get_level()) {
ob_end_clean();
}
// Friendly error pages are used
if (($customError = $this->handleCustomError($proposedException)) !== null) {
return $customError;
}
// If the exception is already our brand, use it
if ($proposedException instanceof ExceptionBase) {
$exception = $proposedException;
}
// If there is an active mask prepared, use that
elseif (static::$activeMask !== null) {
$exception = static::$activeMask;
$exception->setMask($proposedException);
}
// Otherwise we should mask it with our own default scent
else {
$exception = new ApplicationException($proposedException->getMessage(), 0);
$exception->setMask($proposedException);
}
return $this->handleDetailedError($exception);
}
/**
* isNotFoundException returns true if the exception is 404-flavored
*/
protected function isNotFoundException($exception)
{
foreach ($this->notFoundExceptions as $type) {
if ($exception instanceof $type) {
return true;
}
}
return false;
}
/**
* applyMask prepares a mask exception to be used when any exception fires.
* @param Exception $exception The mask exception.
* @return void
*/
public static function applyMask(Throwable $exception)
{
if (static::$activeMask !== null) {
array_push(static::$maskLayers, static::$activeMask);
}
static::$activeMask = $exception;
}
/**
* removeMask destroys the prepared mask by applyMask()
* @return void
*/
public static function removeMask()
{
if (count(static::$maskLayers) > 0) {
static::$activeMask = array_pop(static::$maskLayers);
}
else {
static::$activeMask = null;
}
}
/**
* getDetailedMessage returns a more descriptive error message.
* @param Exception $exception
* @return string
*/
public static function getDetailedMessage($exception)
{
return sprintf(
'"%s" on line %s of %s',
$exception->getMessage(),
$exception->getLine(),
$exception->getFile()
);
}
//
// Overrides
//
/**
* handleCustomError checks if using a custom error page, if so return the contents.
* Return NULL if a custom error is not set up.
* @return mixed
*/
public function handleCustomError($exception)
{
}
/**
* handleDetailedError displays the detailed system exception page.
* @return View Object containing the error page.
*/
public function handleDetailedError($exception)
{
return 'Error: ' . $exception->getMessage();
}
}
================================================
FILE: src/Exception/ExceptionBase.php
================================================
className === null) {
$this->className = get_called_class();
}
if ($this->errorType === null) {
$this->errorType = 'Undefined';
}
parent::__construct($message, $code, $previous);
}
/**
* getClassName returns the class name of the called Exception.
* @return string
*/
public function getClassName()
{
return $this->className;
}
/**
* getErrorType returns the error type derived from the error code used.
* @return string
*/
public function getErrorType()
{
return $this->errorType;
}
/**
* getNiceFile returns a file that is suitable for sharing.
* @return string
*/
public function getNiceFile()
{
return File::nicePath($this->getFile());
}
/**
* mask an exception with the called class. This should catch fatal and php errors.
* It should always be followed by the unmask() method to remove the mask.
* @param string $message Error message.
* @param int $code Error code.
* @return void
*/
public static function mask($message = null, $code = 0)
{
$calledClass = get_called_class();
$exception = new $calledClass($message, $code);
ErrorHandler::applyMask($exception);
}
/**
* unmask removes the active mask from the called class.
*/
public static function unmask()
{
ErrorHandler::removeMask();
}
/**
* setMask is used if this exception acts as a mask, sets the face for the foreign exception.
* @param Throwable $exception Face for the mask, the underlying exception.
* @return void
*/
public function setMask(Throwable $exception)
{
$this->mask = $exception;
$this->applyMask($exception);
}
/**
* applyMask is used if this method is used when applying the mask exception to the face
* exception. It can be used as an override for child classes who may use different
* masking logic.
* @param Throwable $exception Face exception being masked.
* @return void
*/
public function applyMask(Throwable $exception)
{
$this->file = $exception->getFile();
$this->message = $exception->getMessage();
$this->line = $exception->getLine();
$this->className = get_class($exception);
}
/**
* getTrueException is used if this exception is acting as a mask, return the face exception.
* Otherwise return this exception as the true one.
* @return Throwable The underlying exception, or this exception if no mask is applied.
*/
public function getTrueException()
{
if ($this->mask !== null) {
return $this->mask;
}
return $this;
}
/**
* getHighlight generates information used for highlighting the area of code in context of the
* exception line number. The highlighted block of code will be six (6) lines before and after
* the problem line number.
* @return object Highlight information as an array, the following keys are supplied:
* startLine - The starting line number, 6 lines before the error line.
* endLine - The ending line number, 6 lines after the error line.
* errorLine - The focused error line number.
* lines - An array of all the lines to be highlighted, each value is a line of code.
*/
public function getHighlight()
{
if ($this->highlight !== null) {
return $this->highlight;
}
if (!$this->fileContent && File::exists($this->file) && is_readable($this->file)) {
$this->fileContent = @file($this->file) ?: [];
}
$errorLine = $this->line - 1;
$startLine = $errorLine - 6;
if ($startLine < 0) {
$startLine = 0;
}
$endLine = $startLine + 12;
$lineNum = count($this->fileContent);
if ($endLine > $lineNum-1) {
$endLine = $lineNum-1;
}
$areaLines = array_slice($this->fileContent, $startLine, $endLine - $startLine + 1);
$result = [
'startLine' => $startLine,
'endLine' => $endLine,
'errorLine' => $errorLine,
'lines' => []
];
foreach ($areaLines as $index => $line) {
$result['lines'][$startLine + $index] = $line;
}
return $this->highlight = (object) $result;
}
/**
* getHighlightLines returns an array of line numbers used for highlighting the problem area
* of code. This will be six (6) lines before and after the error line number.
* @return array Array of code lines.
*/
public function getHighlightLines()
{
$lines = $this->getHighlight()->lines;
foreach ($lines as $index => $line) {
$lines[$index] = strlen(trim($line)) ? htmlentities($line) : ' '.PHP_EOL;
}
return $lines;
}
/**
* getCallStack returns the call stack as an array containing a stack information object.
* @return Array with stack information, each value will be an object with these values:
* id - The stack ID number.
* code - The class and function name being called.
* args - The arguments passed to the code function above.
* file - Reference to the file containing the called code.
* line - Reference to the line number of the file.
*/
public function getCallStack()
{
$result = [];
$traceInfo = $this->filterCallStack($this->getTrueException()->getTrace());
$lastIndex = count($traceInfo) - 1;
foreach ($traceInfo as $index => $event) {
if (!isset($event['function'])) {
$event['function'] = null;
}
$functionName = (isset($event['class']) && strlen($event['class']))
? $event['class'].$event['type'].$event['function']
: $event['function'];
$file = isset($event['file']) ? '~'.File::localToPublic($event['file']) : null;
$line = $event['line'] ?? null;
$args = null;
if (isset($event['args']) && count($event['args'])) {
$args = $this->formatStackArguments($event['args'], false);
}
$result[] = (object)[
'id' => $lastIndex - $index + 1,
'code' => $functionName,
'args' => $args ? htmlentities($args) : '',
'file' => $file,
'line' => $line
];
}
return $result;
}
/**
* filterCallStack removes the final steps of a call stack, which add no value for the user.
* The following exceptions and any trace information afterwards will be filtered:
* - Illuminate\Foundation\Bootstrap\HandleExceptions
*
* @param array $traceInfo The trace information from getTrace() or debug_backtrace().
* @return array The filtered array containing the trace information.
*/
protected function filterCallStack($traceInfo)
{
/*
* Determine if filter should be used at all.
*/
$useFilter = false;
foreach ($traceInfo as $event) {
if (
isset($event['class']) &&
$event['class'] === 'Illuminate\Foundation\Bootstrap\HandleExceptions' &&
$event['function'] === 'handleError'
) {
$useFilter = true;
}
}
if (!$useFilter) {
return $traceInfo;
}
$filterResult = [];
$pruneResult = true;
foreach ($traceInfo as $index => $event) {
/*
* Prune the tail end of the trace from the framework exception handler.
*/
if (
isset($event['class']) &&
$event['class'] === 'Illuminate\Foundation\Bootstrap\HandleExceptions' &&
$event['function'] === 'handleError'
) {
$pruneResult = false;
continue;
}
if ($pruneResult) {
continue;
}
$filterResult[$index] = $event;
}
return $filterResult;
}
/**
* formatStackArguments prepares a function or method argument list for display in HTML or text format
* @param array $arguments A list of the function or method arguments
* @return string
*/
protected function formatStackArguments($arguments)
{
$argsArray = [];
foreach ($arguments as $argument) {
$arg = null;
if (is_array($argument)) {
$items = [];
foreach ($argument as $index => $obj) {
if (is_array($obj)) {
$value = 'array('.count($obj).')';
}
elseif (is_object($obj)) {
$value = 'object('.get_class($obj).')';
}
elseif (is_int($obj)) {
$value = $obj;
}
elseif ($obj === null) {
$value = "null";
}
else {
$value = "'".$obj."'";
}
$items[] = $index . ' => ' . $value;
}
if (count($items)) {
$arg = 'array(' . count($argument) . ') [' . implode(', ', $items) . ']';
}
else {
$arg = 'array(0)';
}
}
elseif (is_object($argument)) {
$arg = 'object('.get_class($argument).')';
}
elseif ($argument === null) {
$arg = "null";
}
elseif (is_int($argument)) {
$arg = $argument;
}
else {
$arg = "'".$argument."'";
}
$argsArray[] = $arg;
}
return implode(', ', $argsArray);
}
}
================================================
FILE: src/Exception/ForbiddenException.php
================================================
resolveToValidator($validation));
$this->evalErrors();
}
/**
* resolveToValidator resolves general input for the validation exception
* @param mixed $validation
*/
protected function resolveToValidator($validation)
{
$validator = $validation;
if (is_null($validation)) {
$validator = ValidatorFacade::make([], []);
}
elseif (is_array($validation)) {
$validator = ValidatorFacade::make([], []);
$validator->errors()->merge($validation);
}
elseif ($validation instanceof MessageBag) {
$validator = ValidatorFacade::make([], []);
$validator->errors()->merge($validation->messages());
}
if (!$validator instanceof Validator) {
throw new InvalidArgumentException('ValidationException constructor requires instance of Validator or array');
}
return $validator;
}
/**
* evalErrors evaluates errors
*/
protected function evalErrors()
{
$this->fields = [];
foreach ($this->errors() as $field => $messages) {
$fieldName = implode('.', array_merge($this->fieldPrefix, [$field]));
$this->fields[$fieldName] = (array) $messages;
}
$this->message = $this->getErrors()->first();
}
/**
* getErrors returns directly the message bag instance with the model's errors
* @return \Illuminate\Support\MessageBag
*/
public function getErrors()
{
return $this->validator->errors();
}
/**
* @deprecated use ->errors()
*/
public function getFields()
{
return $this->fields;
}
/**
* setFieldPrefix increases the field target specificity
*/
public function setFieldPrefix(array $prefix)
{
$this->fieldPrefix = array_filter($prefix, 'strlen');
$this->evalErrors();
$this->validator = $this->resolveToValidator($this->fields);
}
}
================================================
FILE: src/Extension/Container.php
================================================
extendableConstruct();
}
/**
* __get an undefined property
*/
public function __get($name)
{
return $this->extendableGet($name);
}
/**
* __set an undefined property
*/
public function __set($name, $value)
{
$this->extendableSet($name, $value);
}
/**
* __call calls an undefined local method
*/
public function __call($name, $params)
{
return $this->extendableCall($name, $params);
}
/**
* __callStatic calls an undefined static method
*/
public static function __callStatic($name, $params)
{
return self::extendableCallStatic($name, $params);
}
/**
* __sleep prepare the object for serialization.
*/
public function __sleep()
{
$this->extendableDestruct();
return array_keys(get_object_vars($this));
}
/**
* __wakeup when a model is being unserialized, check if it needs to be booted.
*/
public function __wakeup()
{
$this->extendableConstruct();
}
/**
* extend this class with a closure
*/
public static function extend(callable $callback)
{
self::extendableExtendCallback($callback);
}
}
================================================
FILE: src/Extension/ExtendableTrait.php
================================================
[],
'methods' => [],
'dynamicMethods' => [],
'dynamicProperties' => []
];
/**
* @var array extendableStaticMethods is a collection of static methods used by behaviors
*/
protected static $extendableStaticMethods = [];
/**
* extendableConstruct should be called as part of the constructor
*/
public function extendableConstruct()
{
// Apply init callbacks
$classes = array_merge([static::class], class_parents(static::class));
foreach ($classes as $class) {
if (isset(Container::$classCallbacks[$class]) && is_array(Container::$classCallbacks[$class])) {
foreach (Container::$classCallbacks[$class] as $callback) {
call_user_func($callback, $this);
}
}
}
// Apply extensions
foreach ($this->extensionExtractImplements() as $useClass) {
// Previously soft implemented behaviors started with @ (backward compatibility)
if (substr($useClass, 0, 1) === '@') {
$useClass = substr($useClass, 1);
}
if (!class_exists($useClass)) {
continue;
}
$this->extendClassWith($useClass);
}
}
/**
* extendableDestruct should be called when serializing the object
*/
public function extendableDestruct()
{
$this->extensionData = [
'extensions' => [],
'methods' => [],
'dynamicMethods' => [],
'dynamicProperties' => []
];
}
/**
* extendableExtendCallback is a helper method for `::extend()` static method
* @param callable $callback
* @return void
*/
public static function extendableExtendCallback($callback)
{
$class = get_called_class();
if (
!isset(Container::$classCallbacks[$class]) ||
!is_array(Container::$classCallbacks[$class])
) {
Container::$classCallbacks[$class] = [];
}
Container::$classCallbacks[$class][] = $callback;
}
/**
* extensionExtractImplements will return classes to implement.
*/
protected function extensionExtractImplements(): array
{
if (!$this->implement) {
return [];
}
if (is_string($this->implement)) {
$uses = explode(',', $this->implement);
}
elseif (is_array($this->implement)) {
$uses = $this->implement;
}
else {
throw new Exception(sprintf('Class %s contains an invalid $implement value', static::class));
}
foreach ($uses as &$use) {
$use = str_replace('.', '\\', trim($use));
}
return $uses;
}
/**
* extensionExtractMethods extracts the available methods from a behavior and adds it
* to the list of callable methods
* @param string $extensionName
* @param object $extensionObject
* @return void
*/
protected function extensionExtractMethods($extensionName, $extensionObject)
{
if (!method_exists($extensionObject, 'extensionIsHiddenMethod')) {
throw new Exception(sprintf(
'Extension %s should inherit October\Rain\Extension\ExtensionBase or implement October\Rain\Extension\ExtensionTrait.',
$extensionName
));
}
$extensionMethods = get_class_methods($extensionName);
foreach ($extensionMethods as $methodName) {
if (
$methodName === '__construct' ||
$extensionObject->extensionIsHiddenMethod($methodName)
) {
continue;
}
$this->extensionData['methods'][$methodName] = $extensionName;
}
}
/**
* addDynamicMethod programmatically adds a method to the extendable class
* @param string $dynamicName
* @param callable $method
* @param string $extension
*/
public function addDynamicMethod($dynamicName, $method, $extension = null)
{
if (
is_string($method) &&
$extension &&
($extensionObj = $this->getClassExtension($extension))
) {
$method = [$extensionObj, $method];
}
$this->extensionData['dynamicMethods'][$dynamicName] = $method;
}
/**
* addDynamicProperty programmatically adds a property to the extendable class
* @param string $dynamicName
* @param string $value
*/
public function addDynamicProperty($dynamicName, $value = null)
{
if (
property_exists($this, $dynamicName) ||
array_key_exists($dynamicName, $this->extensionData['dynamicProperties'])
) {
return;
}
$this->extensionData['dynamicProperties'][$dynamicName] = $value;
}
/**
* extendClassWith dynamically extends a class with a specified behavior
* @param string $extensionName
* @return void
*/
public function extendClassWith($extensionName)
{
if (!strlen($extensionName)) {
return;
}
$extensionName = str_replace('.', '\\', trim($extensionName));
if (isset($this->extensionData['extensions'][$extensionName])) {
throw new Exception(sprintf(
'Class %s has already been extended with %s',
static::class,
$extensionName
));
}
$this->extensionData['extensions'][$extensionName] = $extensionObject = new $extensionName($this);
$this->extensionExtractMethods($extensionName, $extensionObject);
$extensionObject->extensionApplyInitCallbacks();
}
/**
* isClassExtendedWith checks if extendable class is extended with a behavior object
* @param string $name Fully qualified behavior name
* @return boolean
*/
public function isClassExtendedWith($name)
{
$name = str_replace('.', '\\', trim($name));
return isset($this->extensionData['extensions'][$name]);
}
/**
* implementClassWith will implement an extension using non-interference and should
* be used with the static extend() method.
*/
public function implementClassWith($extensionName)
{
$extensionName = str_replace('.', '\\', trim($extensionName));
if (in_array($extensionName, $this->extensionExtractImplements())) {
return;
}
$this->implement[] = $extensionName;
}
/**
* isClassInstanceOf checks if the class implements the supplied interface methods.
*/
public function isClassInstanceOf($interface): bool
{
$classMethods = $this->getClassMethods();
if (is_string($interface) && !interface_exists($interface)) {
throw new Exception(sprintf(
'Interface %s does not exist',
$interface
));
}
$interfaceMethods = (array) get_class_methods($interface);
foreach ($interfaceMethods as $methodName) {
if (!in_array($methodName, $classMethods)) {
return false;
}
}
return true;
}
/**
* getClassExtension returns a behavior object from an extendable class, example:
*
* $this->getClassExtension('Backend.Behaviors.FormController')
*
* @param string $name Fully qualified behavior name
* @return mixed
*/
public function getClassExtension($name)
{
$name = str_replace('.', '\\', trim($name));
return $this->extensionData['extensions'][$name] ?? null;
}
/**
* asExtension is short hand for `getClassExtension()` method, except takes the short
* extension name, example:
*
* $this->asExtension('FormController')
*
* @param string $shortName
* @return mixed
*/
public function asExtension($shortName)
{
foreach ($this->extensionData['extensions'] as $class => $obj) {
if (
preg_match('@\\\\([\w]+)$@', $class, $matches) &&
$matches[1] === $shortName
) {
return $obj;
}
}
return $this->getClassExtension($shortName);
}
/**
* methodExists checks if a method exists, extension equivalent of method_exists()
* @param string $name
* @return boolean
*/
public function methodExists($name)
{
return (
method_exists($this, $name) ||
isset($this->extensionData['methods'][$name]) ||
isset($this->extensionData['dynamicMethods'][$name])
);
}
/**
* getClassMethods gets a list of class methods, extension equivalent of get_class_methods()
* @return array
*/
public function getClassMethods()
{
return array_values(array_unique(array_merge(
get_class_methods($this),
array_keys($this->extensionData['methods']),
array_keys($this->extensionData['dynamicMethods'])
)));
}
/**
* getClassMethodAsReflector
*/
public function getClassMethodAsReflector(string $name): ReflectionFunctionAbstract
{
$extendableMethod = $this->getExtendableMethodFromExtensions($name);
if ($extendableMethod !== null) {
return new ReflectionMethod($extendableMethod[0], $extendableMethod[1]);
}
$extendableDynamicMethod = $this->getExtendableMethodFromDynamicMethods($name);
if ($extendableDynamicMethod !== null) {
return new ReflectionFunction($extendableDynamicMethod);
}
return new ReflectionMethod($this, $name);
}
/**
* getDynamicProperties returns all dynamic properties and their values
* @return array ['property' => 'value']
*/
public function getDynamicProperties()
{
return $this->extensionData['dynamicProperties'];
}
/**
* propertyExists checks if a property exists, extension equivalent of `property_exists()`
* @param string $name
* @return boolean
*/
public function propertyExists($name)
{
if (property_exists($this, $name)) {
return true;
}
foreach ($this->extensionData['extensions'] as $extensionObject) {
if (
property_exists($extensionObject, $name) &&
$this->extendableIsAccessible($extensionObject, $name)
) {
return true;
}
}
if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {
return true;
}
return false;
}
/**
* extendableIsAccessible checks if a property is accessible, property equivalent
* of `is_callable()`
* @param mixed $class
* @param string $propertyName
* @return boolean
*/
protected function extendableIsAccessible($class, $propertyName)
{
$reflector = new ReflectionClass($class);
$property = $reflector->getProperty($propertyName);
return $property->isPublic();
}
/**
* extendableGet magic method for `__get()`
* @param string $name
* @return string
*/
public function extendableGet($name)
{
foreach ($this->extensionData['extensions'] as $extensionObject) {
if (
property_exists($extensionObject, $name) &&
$this->extendableIsAccessible($extensionObject, $name)
) {
return $extensionObject->{$name};
}
}
// Getting a dynamic property
if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {
return $this->extensionData['dynamicProperties'][$name];
}
$parent = get_parent_class(self::class);
if ($parent !== false && method_exists($parent, '__get')) {
return parent::__get($name);
}
}
/**
* extendableSet magic method for `__set()`
* @param string $name
* @param string $value
* @return string
*/
public function extendableSet($name, $value)
{
$found = false;
// Spin over each extension to find it
foreach ($this->extensionData['extensions'] as $extensionObject) {
if (!property_exists($extensionObject, $name)) {
continue;
}
$extensionObject->{$name} = $value;
$found = true;
}
// Setting a dynamic property
if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {
$this->extensionData['dynamicProperties'][$name] = $value;
return;
}
// This targets trait usage in particular
$parent = get_parent_class(self::class);
if ($parent !== false && method_exists($parent, '__set')) {
parent::__set($name, $value);
$found = true;
}
// Undefined property, throw an exception to catch it
if (!$found) {
throw new BadMethodCallException(sprintf(
'Call to undefined property %s::%s',
static::class,
$name
));
}
}
/**
* extendableCall magic method for `__call()`
* @param string $name
* @param array $params
* @return mixed
*/
public function extendableCall($name, $params = null)
{
$callable = $this->getExtendableMethodFromExtensions($name);
if ($callable === null) {
$callable = $this->getExtendableMethodFromDynamicMethods($name);
}
if ($callable !== null) {
return call_user_func_array($callable, $params);
}
$parent = get_parent_class(self::class);
if ($parent !== false && method_exists($parent, '__call')) {
return parent::__call($name, $params);
}
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()',
static::class,
$name
));
}
/**
* extendableCallStatic magic method for `__callStatic()`
* @param string $name
* @param array $params
* @return mixed
*/
public static function extendableCallStatic($name, $params = null)
{
$className = get_called_class();
if (!array_key_exists($className, self::$extendableStaticMethods)) {
self::$extendableStaticMethods[$className] = [];
$class = new ReflectionClass($className);
$defaultProperties = $class->getDefaultProperties();
if (
array_key_exists('implement', $defaultProperties) &&
($implement = $defaultProperties['implement'])
) {
// Apply extensions
if (is_string($implement)) {
$uses = explode(',', $implement);
}
elseif (is_array($implement)) {
$uses = $implement;
}
else {
throw new Exception(sprintf('Class %s contains an invalid $implement value', $className));
}
foreach ($uses as $use) {
$useClassName = str_replace('.', '\\', trim($use));
$useClass = new ReflectionClass($useClassName);
$staticMethods = $useClass->getMethods(ReflectionMethod::IS_STATIC);
foreach ($staticMethods as $method) {
self::$extendableStaticMethods[$className][$method->getName()] = $useClassName;
}
}
}
}
if (isset(self::$extendableStaticMethods[$className][$name])) {
$extension = self::$extendableStaticMethods[$className][$name];
if (method_exists($extension, $name) && is_callable([$extension, $name])) {
$extension::$extendableStaticCalledClass = $className;
$result = forward_static_call_array(array($extension, $name), $params);
$extension::$extendableStaticCalledClass = null;
return $result;
}
}
// $parent = get_parent_class($className);
// if ($parent !== false && method_exists($parent, '__callStatic')) {
// return parent::__callStatic($name, $params);
// }
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()',
$className,
$name
));
}
/**
* getExtendableMethodFromExtensions
*/
protected function getExtendableMethodFromExtensions(string $name): ?array
{
if (!isset($this->extensionData['methods'][$name])) {
return null;
}
$extension = $this->extensionData['methods'][$name];
$extensionObject = $this->extensionData['extensions'][$extension];
if (!method_exists($extension, $name) || !is_callable([$extensionObject, $name])) {
return null;
}
return [$extensionObject, $name];
}
/**
* getExtendableMethodFromDynamicMethods
*/
protected function getExtendableMethodFromDynamicMethods(string $name): ?callable
{
if (!isset($this->extensionData['dynamicMethods'][$name])) {
return null;
}
$dynamicCallable = $this->extensionData['dynamicMethods'][$name];
if (!is_callable($dynamicCallable)) {
return null;
}
return $dynamicCallable;
}
/**
* @deprecated use \October\Rain\Extension\Container::clearExtensions()
*/
public static function clearExtendedClasses()
{
Container::clearExtensions();
}
}
================================================
FILE: src/Extension/ExtensionBase.php
================================================
['extensionIsHiddenProperty', 'extensionIsHiddenMethod'],
'properties' => []
];
/**
* extensionApplyInitCallbacks
*/
public function extensionApplyInitCallbacks()
{
$classes = array_merge([static::class], class_parents($this));
foreach ($classes as $class) {
if (isset(Container::$extensionCallbacks[$class]) && is_array(Container::$extensionCallbacks[$class])) {
foreach (Container::$extensionCallbacks[$class] as $callback) {
call_user_func($callback, $this);
}
}
}
}
/**
* extensionExtendCallback is a helper method for `::extend()` static method
* @param callable $callback
* @return void
*/
public static function extensionExtendCallback($callback)
{
$class = get_called_class();
if (
!isset(Container::$extensionCallbacks[$class]) ||
!is_array(Container::$extensionCallbacks[$class])
) {
Container::$extensionCallbacks[$class] = [];
}
Container::$extensionCallbacks[$class][] = $callback;
}
/**
* extensionHideMethod
*/
protected function extensionHideMethod($name)
{
$this->extensionHidden['methods'][] = $name;
}
/**
* extensionHideProperty
*/
protected function extensionHideProperty($name)
{
$this->extensionHidden['properties'][] = $name;
}
/**
* extensionIsHiddenMethod
*/
public function extensionIsHiddenMethod($name)
{
return in_array($name, $this->extensionHidden['methods']);
}
/**
* extensionIsHiddenProperty
*/
public function extensionIsHiddenProperty($name)
{
return in_array($name, $this->extensionHidden['properties']);
}
/**
* getCalledExtensionClass
*/
public static function getCalledExtensionClass()
{
return self::$extendableStaticCalledClass;
}
}
================================================
FILE: src/Extension/README.md
================================================
## Rain Extensions
Adds the ability for classes to have *private traits*, also known as Behaviors. These are similar to native PHP Traits except they have some distinct benefits:
1. Behaviors have their own constructor.
1. Behaviors can have private or protected methods.
1. Methods and property names can conflict safely.
1. Class can be extended with behaviors dynamically.
Where you might use a trait like this:
class MyClass
{
use \October\Rain\UtilityFunctions;
use \October\Rain\DeferredBinding;
}
A behavior is used in a similar fashion:
class MyClass extends \October\Rain\Extension\Extendable
{
public $implement = [
'October.Rain.UtilityFunctions',
'October.Rain.DeferredBinding',
];
}
Where you might define a trait like this:
trait UtilityFunctions
{
public function sayHello()
{
echo "Hello from " . get_class($this);
}
}
A behavior is defined like this:
class UtilityFunctions extends \October\Rain\Extension\ExtensionBase
{
protected $parent;
public function __construct($parent)
{
$this->parent = $parent;
}
public function sayHello()
{
echo "Hello from " . get_class($this->parent);
}
}
The extended object is always passed as the first parameter to the Behavior's constructor.
### Usage example
#### Behavior / Extension class
controller = $controller;
}
public function someMethod()
{
return "I come from the FormController Behavior!";
}
public function otherMethod()
{
return "You might not see me...";
}
}
#### Extending a class
This `Controller` class will implement the `FormController` behavior and then the methods will become available (mixed in) to the class. We will override the `otherMethod` method.
someMethod();
// Prints: I come from the main Controller!
echo $controller->otherMethod();
// Prints: You might not see me...
echo $controller->asExtension('FormController')->otherMethod();
### Dynamically using a behavior / Constructor extension
Any class that uses the `Extendable` or `ExtendableTrait` can have its constructor extended with the static `extend()` method. The argument should pass a closure that will be called as part of the class constructor. For example:
/**
* Extend the Pizza Shop to include the Master Splinter behavior too
*/
MyNamespace\Controller::extend(function($controller){
// Implement the list controller behavior dynamically
$controller->implement[] = 'MyNamespace.Behaviors.ListController';
});
### Dynamically creating methods
Methods can be added to a `Model` through the use of `addDynamicMethod`.
Post::extend(function($model) {
$model->addDynamicMethod('getTagsAttribute', function() use ($model) {
return $model->tags()->lists('name');
});
});
### Soft definition
If a behavior class does not exist, like a trait, an *Class not found* error will be thrown. In some cases you may wish to suppress this error, for conditional implementation if a module is present in the system. You can do this by placing an `@` symbol at the beginning of the class name.
class User extends \October\Rain\Extension\Extendable
{
public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel'];
}
If the class name `RainLab\Translate\Behaviors\TranslatableModel` does not exist, no error will be thrown. This is the equivalent of the following code:
class User extends \October\Rain\Extension\Extendable
{
public $implement = [];
public function __construct()
{
if (class_exists('RainLab\Translate\Behaviors\TranslatableModel')) {
$controller->implement[] = 'RainLab.Translate.Behaviors.TranslatableModel';
}
parent::__construct();
}
}
### Using Traits instead of base classes
In some cases you may not wish to extend the `ExtensionBase` or `Extendable` classes, due to other needs. So you can use the traits instead, although obviously the behavior methods will not be available to the parent class.
- When using the `ExtensionTrait` the methods from `ExtensionBase` should be applied to the class.
- When using the `ExtendableTrait` the methods from `Extendable` should be applied to the class.
================================================
FILE: src/Filesystem/Definitions.php
================================================
getDefinitions($type);
}
/**
* getDefinitions returns a definition set from config or from the default sets.
*/
public function getDefinitions(string $type): array
{
$typeConfig = snake_case($type);
$typeMethod = studly_case($type);
if (!method_exists($this, $typeMethod)) {
throw new Exception(sprintf('No such definition set exists for "%s"', $type));
}
// Support dual configuration
return (array) Config::get('media.'.$typeConfig,
// @deprecated
Config::get('cms.file_definitions.'.$typeConfig,
$this->$typeMethod()
)
);
}
/**
* isPathIgnored determines if a path should be ignored
* @param string $path
* @return boolean
*/
public static function isPathIgnored($path)
{
$ignoreNames = self::get('ignore_files');
$ignorePatterns = self::get('ignore_patterns');
if (in_array($path, $ignoreNames)) {
return true;
}
foreach ($ignorePatterns as $pattern) {
if (preg_match('/'.$pattern.'/', $path)) {
return true;
}
}
return false;
}
/**
* ignoreFiles that can be safely ignored.
* This list can be customized with config:
* - media.ignore_files
*/
protected function ignoreFiles()
{
return [
'.svn',
'.git',
'.DS_Store',
'.AppleDouble'
];
}
/**
* ignorePatterns that can be safely ignored.
* This list can be customized with config:
* - media.ignore_patterns
*/
protected function ignorePatterns()
{
return [
'^\..*'
];
}
/**
* defaultExtensions that are particularly benign.
* This list can be customized with config:
* - media.default_extensions
*/
protected function defaultExtensions()
{
return [
'jpg',
'jpeg',
'bmp',
'png',
'webp',
'avif',
'gif',
'svg',
'js',
'map',
'ico',
'css',
'less',
'scss',
'ics',
'odt',
'doc',
'docx',
'ppt',
'pptx',
'pdf',
'swf',
'txt',
'ods',
'xls',
'xlsx',
'eot',
'woff',
'woff2',
'ttf',
'flv',
'wmv',
'mp3',
'ogg',
'wav',
'avi',
'mov',
'mp4',
'mpeg',
'webm',
'mkv',
'rar',
'zip'
];
}
/**
* assetExtensions seen as public assets.
* This list can be customized with config:
* - media.asset_extensions
*/
protected function assetExtensions()
{
return [
'jpg',
'jpeg',
'bmp',
'png',
'webp',
'avif',
'gif',
'ico',
'css',
'js',
'woff',
'woff2',
'svg',
'ttf',
'eot',
'json',
'md',
'less',
'sass',
'scss'
];
}
/**
* imageExtensions typically used as images.
* This list can be customized with config:
* - media.image_extensions
*/
protected function imageExtensions()
{
return [
'jpg',
'jpeg',
'bmp',
'png',
'webp',
'avif',
'gif'
];
}
/**
* videoExtensions typically used as video files.
* This list can be customized with config:
* - media.video_extensions
*/
protected function videoExtensions()
{
return [
'mp4',
'avi',
'mov',
'mpg',
'mpeg',
'mkv',
'webm'
];
}
/**
* audioExtensions typically used as audio files.
* This list can be customized with config:
* - media.audio_extensions
*/
protected function audioExtensions()
{
return [
'mp3',
'wav',
'wma',
'm4a',
'ogg'
];
}
}
================================================
FILE: src/Filesystem/Filesystem.php
================================================
= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
}
if ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
}
if ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
}
if ($bytes > 1) {
return $bytes . ' bytes';
}
if ($bytes === 1) {
return $bytes . ' byte';
}
return '0 bytes';
}
/**
* localToPublic returns a public file path from an absolute one
* eg: /home/mysite/public_html/welcome -> /welcome
* @param string $path Absolute path
* @return string
*/
public function localToPublic($path)
{
/**
* @event filesystem.localToPublic
* Allow custom logic for converting local to public paths on non-standard installations.
*
* Example usage
*
* Event::listen('filesystem.localToPublic', function ($path) {
* return '/custom/public/path';
* });
*/
if (($event = Event::fire('filesystem.localToPublic', [$path], true)) !== null) {
return $event;
}
// Check real paths
$basePath = base_path();
if (strpos($path, $basePath) === 0) {
return str_replace("\\", "/", substr($path, strlen($basePath)));
}
// Check first level symlinks
foreach ($this->getRootSymlinks() as $dir) {
$resolvedDir = readlink($dir);
if (strpos($path, $resolvedDir) === 0) {
$relativePath = substr($path, strlen($resolvedDir));
return str_replace("\\", "/", substr($dir, strlen($basePath)) . $relativePath);
}
}
return null;
}
/**
* getRootSymlinks returns any unresolved symlinks in the public directory
*/
protected function getRootSymlinks(): array
{
if ($this->symlinkRootCache === null) {
$symDirs = [];
foreach ($this->directories(base_path()) as $dir) {
if (is_link($dir)) {
$symDirs[] = $dir;
}
}
$this->symlinkRootCache = $symDirs;
}
return $this->symlinkRootCache;
}
/**
* isLocalPath returns true if the specified path is within the path of the application.
* realpath resolves the provided path before checking location, set to false if you need
* to check if a potentially non-existent path would be within the application path.
* @param string $path
* @param bool $realpath
* @return bool
*/
public function isLocalPath($path, $realpath = true)
{
$base = base_path();
if ($realpath) {
$path = realpath($path);
}
return !($path === false || strncmp($path, $base, strlen($base)) !== 0);
}
/**
* fromClass finds the path to a class
* @param mixed $className Class name or object
* @return string The file path
*/
public function fromClass($className)
{
$reflector = new ReflectionClass($className);
return $reflector->getFileName();
}
/**
* existsInsensitive determines if a file exists with case insensitivity
* supported for the file only. Returne either the sensitive path or false.
* @param string $path
* @return string|bool
*/
public function existsInsensitive($path)
{
if ($this->exists($path)) {
return $path;
}
$directoryName = dirname($path);
$pathLower = strtolower($path);
if (!$files = $this->glob($directoryName . '/*', GLOB_NOSORT)) {
return false;
}
foreach ($files as $file) {
if (strtolower($file) === $pathLower) {
return $file;
}
}
return false;
}
/**
* normalizePath returns a normalized version of the supplied path for use in
* combined Windows and Unix systems.
* @param string $path
* @return string
*/
public function normalizePath($path)
{
return str_replace('\\', '/', $path);
}
/**
* nicePath removes the base path from a local path and returns a relatively nice
* path that is suitable and safe for sharing.
* @param string $path
* @return string
*/
public function nicePath($path)
{
return $this->normalizePath(str_replace([
base_path(),
$this->normalizePath(base_path())
], '~', $path));
}
/**
* symbolizePath converts a path using path symbol. Returns the original path if
* no symbol is used and no default is specified.
* @param string $path
* @param mixed $default
* @return string
*/
public function symbolizePath($path, $default = false)
{
if (!$firstChar = $this->isPathSymbol($path)) {
return $default === false ? $path : $default;
}
$_path = substr($path, 1);
return $this->pathSymbols[$firstChar] . $_path;
}
/**
* isPathSymbol returns the symbol if the path uses a symbol, otherwise false
* @param string $path
* @return bool|string
*/
public function isPathSymbol($path)
{
if (!$path) {
return false;
}
$firstChar = substr($path, 0, 1);
if (isset($this->pathSymbols[$firstChar])) {
return $firstChar;
}
return false;
}
/**
* put writes the contents of a file
* @param string $path
* @param string $contents
* @return int
*/
public function put($path, $contents, $lock = false)
{
$result = parent::put($path, $contents, $lock);
$this->chmod($path);
return $result;
}
/**
* copy a file to a new location.
* @param string $path
* @param string $target
* @return bool
*/
public function copy($path, $target)
{
$result = parent::copy($path, $target);
$this->chmod($target);
return $result;
}
/**
* getSafe reads the first portion of file contents
*/
public function getSafe(string $path, float $limitKbs = 1)
{
$limit = $limitKbs * 4096;
$parser = fopen($path, 'r');
return fread($parser, $limit);
}
/**
* makeDirectory creates a directory
* @param string $path
* @param int $mode
* @param bool $recursive
* @param bool $force
* @return bool
*/
public function makeDirectory($path, $mode = 0755, $recursive = false, $force = false)
{
if ($mask = $this->getFolderPermissions()) {
$mode = $mask;
}
// Find the green leaves
if ($recursive && $mask) {
$chmodPath = $path;
while (true) {
$basePath = dirname($chmodPath);
if ($chmodPath === $basePath) {
break;
}
if ($this->isDirectory($basePath)) {
break;
}
$chmodPath = $basePath;
}
}
else {
$chmodPath = $path;
}
// Make the directory
$result = parent::makeDirectory($path, $mode, $recursive, $force);
// Apply the permissions
if ($mask) {
$this->chmod($chmodPath, $mask);
if ($recursive) {
$this->chmodRecursive($chmodPath, null, $mask);
}
}
return $result;
}
/**
* chmod modifies file/folder permissions
* @param string $path
* @param octal $mask
* @return void
*/
public function chmod($path, $mask = null)
{
if (!$mask) {
$mask = $this->isDirectory($path)
? $this->getFolderPermissions()
: $this->getFilePermissions();
}
if (!$mask) {
return;
}
return @chmod($path, $mask);
}
/**
* chmodRecursive modifies file/folder permissions recursively
* @param string $path
* @param octal $fileMask
* @param octal $directoryMask
* @return void
*/
public function chmodRecursive($path, $fileMask = null, $directoryMask = null)
{
if (!$fileMask) {
$fileMask = $this->getFilePermissions();
}
if (!$directoryMask) {
$directoryMask = $this->getFolderPermissions() ?: $fileMask;
}
if (!$fileMask) {
return;
}
if (!$this->isDirectory($path)) {
return $this->chmod($path, $fileMask);
}
$items = new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS);
foreach ($items as $item) {
if ($item->isDir()) {
$_path = $item->getPathname();
$this->chmod($_path, $directoryMask);
$this->chmodRecursive($_path, $fileMask, $directoryMask);
}
else {
$this->chmod($item->getPathname(), $fileMask);
}
}
}
/**
* getFilePermissions returns the default file permission mask to use
* @return string Permission mask as octal (0755) or null
*/
public function getFilePermissions()
{
return $this->filePermissions
? octdec($this->filePermissions)
: null;
}
/**
* getFolderPermissions returns the default folder permission mask to use
* @return string Permission mask as octal (0755) or null
*/
public function getFolderPermissions()
{
return $this->folderPermissions
? octdec($this->folderPermissions)
: null;
}
/**
* fileNameMatch matches filename against a pattern
* @param string|array $fileName
* @param string $pattern
* @return bool
*/
public function fileNameMatch($fileName, $pattern)
{
if ($pattern === $fileName) {
return true;
}
$regex = strtr(preg_quote($pattern, '#'), ['\*' => '.*', '\?' => '.']);
return (bool) preg_match('#^' . $regex . '$#i', $fileName);
}
/**
* lastModifiedRecursive checks an entire directory and
* returns the mtime of the freshest file.
*/
public function lastModifiedRecursive($path)
{
$mtime = 0;
foreach ($this->allFiles($path) as $file) {
$mtime = max($mtime, $this->lastModified($file->getPathname()));
}
return $mtime;
}
/**
* searchDirectory locates a file and return its relative path Eg: Searching
* directory /home/mysite for file index.php could locate this file
* /home/mysite/public_html/welcome/index.php and would return
* public_html/welcome
* @param string $file index.php
* @param string $directory /home/mysite
* @return string public_html/welcome
*/
public function searchDirectory($file, $directory, $rootDir = '')
{
$files = $this->files($directory);
$directories = $this->directories($directory);
foreach ($files as $directoryFile) {
if ($directoryFile->getFileName() === $file) {
return $rootDir;
}
}
foreach ($directories as $subdirectory) {
$relativePath = strlen($rootDir)
? $rootDir.'/'.basename($subdirectory)
: basename($subdirectory);
$result = $this->searchDirectory($file, $subdirectory, $relativePath);
if ($result !== null) {
return $result;
}
}
return null;
}
}
================================================
FILE: src/Filesystem/FilesystemServiceProvider.php
================================================
registerCoreDisks($this->app['config']);
$this->registerNativeFilesystem();
$this->registerFlysystem();
// After registration
$this->app->booting(function () {
$this->configureDefaultPermissions($this->app['config'], $this->app['files']);
});
}
/**
* registerNativeFilesystem implementation.
*/
protected function registerNativeFilesystem()
{
$this->app->singleton('files', function () {
$config = $this->app['config'];
$files = new Filesystem;
$files->filePermissions = $config->get('system.default_mask.file', null);
$files->folderPermissions = $config->get('system.default_mask.folder', null);
$files->pathSymbols = [
'~' => base_path()
];
if ($this->app->has('path.themes')) {
$files->pathSymbols['#'] = themes_path();
}
if ($this->app->has('path.plugins')) {
$files->pathSymbols['$'] = plugins_path();
}
return $files;
});
}
/**
* registerCoreDisks ensures
*/
protected function registerCoreDisks($config)
{
if ($config->get('filesystems.disks.uploads') === null) {
$config->set('filesystems.disks.uploads', [
'driver' => 'local',
'root' => storage_path('app/uploads'),
'url' => '/storage/app/uploads',
'throw' => false,
]);
}
if ($config->get('filesystems.disks.media') === null) {
$config->set('filesystems.disks.media', [
'driver' => 'local',
'root' => storage_path('app/media'),
'url' => '/storage/app/media',
'visibility' => 'public',
'throw' => false,
]);
}
if ($config->get('filesystems.disks.resources') === null) {
$config->set('filesystems.disks.resources', [
'driver' => 'local',
'root' => storage_path('app/resources'),
'url' => '/storage/app/resources',
'visibility' => 'public',
'throw' => false,
]);
}
}
/**
* configureDefaultPermissions
*/
protected function configureDefaultPermissions($config, $files)
{
if ($config->get('filesystems.disks.local.permissions.file.public') === null) {
$config->set('filesystems.disks.local.permissions.file.public', $files->getFilePermissions());
}
if ($config->get('filesystems.disks.local.permissions.dir.public') === null) {
$config->set('filesystems.disks.local.permissions.dir.public', $files->getFolderPermissions());
}
}
}
================================================
FILE: src/Filesystem/README.md
================================================
Filesystem - an extension of illuminate\filesystem
=======
================================================
FILE: src/Filesystem/Zip.php
================================================
add('/some/path/*.php');
*
* // Do not include subdirectories, one level only
* $zip->add('/non/recursive/*', ['recursive' => false]);
*
* // Add multiple paths
* $zip->add([
* '/collection/of/paths/*',
* '/a/single/file.php'
* ]);
*
* // Add all INI files to a zip folder "config"
* $zip->folder('config', '/path/to/config/*.ini');
*
* // Add multiple paths to a zip folder "images"
* $zip->folder('images', function($zip) {
* $zip->add('/my/gifs/*.gif', );
* $zip->add('/photo/reel/*.{png,jpg}', );
* });
*
* // Remove these files/folders from the zip
* $zip->remove([
* '.htaccess',
* 'config.php',
* 'some/folder'
* ]);
*
* });
*
* Zip::extract('file.zip', '/destination/path');
*
*/
use ZipArchive;
class Zip extends ZipArchive
{
/**
* @var string folderPrefix
*/
protected $folderPrefix = '';
/**
* @var array excludedFiles
*/
protected $excludedFiles = [];
/**
* @var array excludedFolders
*/
protected $excludedFolders = [];
/**
* extract an existing zip file.
* @param string $source Path for the existing zip
* @param string $destination Path to extract the zip files
* @param array $options
* @return bool
*/
public static function extract($source, $destination, $options = [])
{
extract(array_merge([
'mask' => 0755
], $options));
if (file_exists($destination) || mkdir($destination, $mask, true)) {
$zip = new ZipArchive;
if ($zip->open($source) === true) {
$zip->extractTo($destination);
$zip->close();
return true;
}
}
return false;
}
/**
* make creates a new empty zip file.
* @param string $destination Path for the new zip
* @param mixed $source
* @param array $options
* @return self
*/
public static function make($destination, $source, $options = [])
{
$zip = new self;
$zip->open($destination, ZIPARCHIVE::CREATE | ZipArchive::OVERWRITE);
if (is_string($source)) {
$zip->add($source, $options);
}
elseif (is_callable($source)) {
$source($zip);
}
elseif (is_array($source)) {
foreach ($source as $_source) {
$zip->add($_source, $options);
}
}
$zip->close();
return $zip;
}
/**
* add includes a source to the Zip
* @param mixed $source
* @param array $options
* @return self
*/
public function add($source, $options = [])
{
/*
* A directory has been supplied, convert it to a useful glob
*
* The wildcard for including hidden files:
* - isn't hidden with an '.'
* - is hidden with a '.' but is followed by a non '.' character
* - starts with '..' but has at least one character after it
*/
if (is_dir($source)) {
$includeHidden = isset($options['includeHidden']) && $options['includeHidden'];
$wildcard = $includeHidden ? '{*,.[!.]*,..?*}' : '*';
$source = implode('/', [dirname($source), basename($source), $wildcard]);
}
extract(array_merge([
'recursive' => true,
'includeHidden' => false,
'basedir' => dirname($source),
'baseglob' => basename($source)
], $options));
if (is_file($source)) {
$files = [$source];
$recursive = false;
}
else {
$files = glob($source, GLOB_BRACE);
$folders = glob(dirname($source) . '/*', GLOB_ONLYDIR);
}
foreach ($files as $file) {
if (!is_file($file)) {
continue;
}
if ($this->isExcluded($file)) {
continue;
}
$localpath = $this->removePathPrefix($basedir.'/', dirname($file).'/');
$localfile = $this->folderPrefix . $localpath . basename($file);
$this->addFile($file, $localfile);
}
if (!$recursive) {
return $this;
}
foreach ($folders as $folder) {
if (!is_dir($folder)) {
continue;
}
if ($this->isExcluded($folder)) {
continue;
}
$localpath = $this->folderPrefix . $this->removePathPrefix($basedir.'/', $folder.'/');
$this->addEmptyDir($localpath);
$this->add($folder.'/'.$baseglob, array_merge($options, ['basedir' => $basedir]));
}
return $this;
}
/**
* folder creates a new folder inside the Zip and adds source files (optional)
* @param string $name Folder name
* @param mixed $source
* @return self
*/
public function folder($name, $source = null)
{
$prefix = $this->folderPrefix;
$this->addEmptyDir($prefix . $name);
if ($source === null) {
return $this;
}
$this->folderPrefix = $prefix . $name . '/';
if (is_string($source)) {
$this->add($source);
}
elseif (is_callable($source)) {
$source($this);
}
elseif (is_array($source)) {
foreach ($source as $_source) {
$this->add($_source);
}
}
$this->folderPrefix = $prefix;
return $this;
}
/**
* remove a file or folder from the zip collection.
* Does not support wildcards.
* @param string $source
* @return self
*/
public function remove($source)
{
if (is_array($source)) {
foreach ($source as $_source) {
$this->remove($_source);
}
}
if (!is_string($source)) {
return $this;
}
if (substr($source, 0, 1) === '/') {
$source = substr($source, 1);
}
for ($i = $this->numFiles - 1; $i >= 0; $i--) {
if (($stats = $this->statIndex($i)) === false) {
continue;
}
if (substr($stats['name'], 0, strlen($source)) === $source) {
$this->deleteIndex($i);
}
}
return $this;
}
/**
* exclude paths from inclusion in the zip file
*/
public function exclude(array $paths): void
{
foreach ($paths as $path) {
if (is_file($path)) {
$this->excludedFiles[] = $path;
}
elseif (is_dir($path)) {
$this->excludedFolders[] = $path;
}
}
}
/**
* isExcluded checks if a path is excluded
*/
public function isExcluded(string $path): bool
{
if ($normalPath = realpath($path)) {
if (is_dir($normalPath)) {
return in_array($normalPath, $this->excludedFolders);
}
elseif (is_file($normalPath)) {
return in_array($normalPath, $this->excludedFiles);
}
}
return false;
}
/**
* removePathPrefix removes a prefix from a path.
* @param string $prefix /var/sites/
* @param string $path /var/sites/moo/cow/
* @return string moo/cow/
*/
protected function removePathPrefix($prefix, $path)
{
return (strpos($path, $prefix) === 0)
? substr($path, strlen($prefix))
: $path;
}
}
================================================
FILE: src/Flash/FlashBag.php
================================================
session = App::make('session');
if ($this->session->has(self::SESSION_KEY)) {
$this->messages = $this->session->get(self::SESSION_KEY);
}
$this->purge();
}
/**
* check to see if any message is available.
* @return bool
*/
public function check()
{
return $this->any();
}
/**
* all gets first message for every key in the bag.
* @param string|null $format
* @return array
*/
public function all($format = null)
{
$all = [];
foreach ($this->messages as $key => $messages) {
$all[$key] = reset($messages);
}
$this->purge();
return $all;
}
/**
* messages
*/
public function messages()
{
$messages = parent::messages();
$this->purge();
return $messages;
}
/**
* get all the flash messages of a given type.
* @param string $key
* @param string|null $format
* @return array
*/
public function get($key, $format = null)
{
$message = parent::get($key, $format);
$this->purge();
return $message;
}
/**
* error gets or sets an error message
* @param string|null $message
* @return array|FlashBag
*/
public function error($message = null)
{
if ($message === null) {
return $this->get(FlashBag::ERROR);
}
return $this->add(FlashBag::ERROR, $message);
}
/**
* Sets Gets / a success message
* @param string|null $message
* @return array|FlashBag
*/
public function success($message = null)
{
if ($message === null) {
return $this->get(FlashBag::SUCCESS);
}
return $this->add(FlashBag::SUCCESS, $message);
}
/**
* Gets / Sets a warning message
* @param string|null $message
* @return array|FlashBag
*/
public function warning($message = null)
{
if ($message === null) {
return $this->get(FlashBag::WARNING);
}
return $this->add(FlashBag::WARNING, $message);
}
/**
* Gets / Sets a information message
* @param string|null $message
* @return array|FlashBag
*/
public function info($message = null)
{
if ($message === null) {
return $this->get(FlashBag::INFO);
}
return $this->add(FlashBag::INFO, $message);
}
/**
* Add a message to the bag and stores it in the session.
*
* @param string $key
* @param string $message
* @return \October\Rain\Flash\FlashBag
*/
public function add($key, $message)
{
$this->newMessages[$key][] = $message;
$this->store();
return parent::add($key, $message);
}
/**
* store the flash data to the session.
*/
public function store()
{
$this->session->put(self::SESSION_KEY, $this->newMessages);
}
/**
* forget removes an object with a specified key or erases the flash data.
* @param string $key Specifies a key to remove, optional
*/
public function forget($key = null)
{
if ($key === null) {
$this->newMessages = $this->messages = [];
$this->purge();
}
else {
if (isset($this->messages[$key])) {
unset($this->messages[$key]);
}
if (isset($this->newMessages[$key])) {
unset($this->newMessages[$key]);
}
$this->store();
}
}
/**
* purge all flash data from the session.
*/
public function purge()
{
$this->session->remove(self::SESSION_KEY);
}
}
================================================
FILE: src/Flash/FlashServiceProvider.php
================================================
app->singleton('flash', function () {
return new FlashBag;
});
$this->app->alias('flash', FlashBag::class);
}
/**
* provides gets the services provided by the provider
*/
public function provides()
{
return ['flash', FlashBag::class];
}
}
================================================
FILE: src/Foundation/Application.php
================================================
withKernels()
->withEvents()
->withCommands()
->withProviders();
}
/**
* registerBaseServiceProviders registers all of the base service providers
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new ContextServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
$this->register(new ExecutionContextProvider($this));
}
/**
* bindPathsInContainer binds all of the application paths in the container
*/
protected function bindPathsInContainer()
{
parent::bindPathsInContainer();
// Additional lang path check
if (is_dir($directory = $this->path('lang'))) {
$this->useLangPath($directory);
}
// October CMS paths
$this->instance('path.plugins', $this->pluginsPath());
$this->instance('path.themes', $this->themesPath());
$this->instance('path.cache', $this->cachePath());
$this->instance('path.temp', $this->tempPath());
}
/**
* publicPath gets the path to the public / web directory
* @return string
*/
public function publicPath($path = '')
{
return $this->hasPublicFolder()
? $this->joinPaths($this->basePath('public'), $path)
: $this->joinPaths($this->basePath, $path);
}
/**
* hasPublicFolder returns true if a public folder exists, initiated by october:mirror
*/
public function hasPublicFolder()
{
return file_exists($this->basePath('public'));
}
/**
* cachePath return path for cache files
* @param string $path
* @return string
*/
public function cachePath($path = '')
{
return $this->joinPaths($this->cachePath ?: $this->basePath('storage'), $path);
}
/**
* useCachePath sets path path for cache files
* @param string $path
* @return $this
*/
public function useCachePath($path)
{
$this->cachePath = $path;
$this->instance('path.cache', $path);
$this->instance('path.temp', $path.DIRECTORY_SEPARATOR.'temp');
return $this;
}
/**
* pluginsPath returns path to location of plugins
* @param string $path
* @return string
*/
public function pluginsPath($path = '')
{
return $this->joinPaths($this->pluginsPath ?: $this->basePath('plugins'), $path);
}
/**
* usePluginsPath sets path to location of plugins
* @param string $path
* @return $this
*/
public function usePluginsPath($path)
{
$this->pluginsPath = $path;
$this->instance('path.plugins', $path);
return $this;
}
/**
* themesPath returns path to location of themes
* @param string $path
* @return string
*/
public function themesPath($path = '')
{
return $this->joinPaths($this->themesPath ?: $this->basePath('themes'), $path);
}
/**
* useThemesPath sets path to location of themes
* @param string $path
* @return $this
*/
public function useThemesPath($path)
{
$this->themesPath = $path;
$this->instance('path.themes', $path);
return $this;
}
/**
* tempPath returns path for storing temporary files.
* @param string $path
* @return string
*/
public function tempPath($path = ''): string
{
return $this->joinPaths($this->cachePath('temp'), $path);
}
/**
* normalizeCachePath normalizes a relative or absolute path to a cache file.
* @param string $key
* @param string $default
* @return string
*/
protected function normalizeCachePath($key, $default)
{
if (is_null($env = Env::get($key))) {
return $this->cachePath($default);
}
return Str::startsWith($env, '/')
? $env
: $this->basePath($env);
}
/**
* before logic is called before the router runs.
* @param \Closure|string $callback
* @return void
*/
public function before($callback)
{
return $this['router']->before($callback);
}
/**
* after logic is called after the router finishes.
* @param \Closure|string $callback
* @return void
*/
public function after($callback)
{
return $this['router']->after($callback);
}
/**
* error registers an application error handler.
* @param \Closure $callback
* @return void
*/
public function error(callable $callback)
{
$this->make(\Illuminate\Contracts\Debug\ExceptionHandler::class)->renderable($callback);
}
/**
* @deprecated use App::error with an Error exception type
*/
public function fatal(callable $callback)
{
$this->error(function(Error $e) use ($callback) {
return $callback($e);
});
}
/**
* runningInBackend determines if we are running in the backend area.
* @return bool
*/
public function runningInBackend()
{
return $this['execution.context'] === 'backend';
}
/**
* runningInFrontend determines if we are running in the frontend area.
* @return bool
*/
public function runningInFrontend()
{
return !$this->runningInBackend() && !$this->runningInConsole();
}
/**
* runningInOctane determines if the application is running under Laravel Octane.
* @return bool
*/
public function runningInOctane()
{
return isset($_ENV['OCTANE_SERVER']) && $_ENV['OCTANE_SERVER'];
}
/**
* hasDatabase returns true if a database connection is present.
* @return boolean
*/
public function hasDatabase()
{
try {
$this['db.connection']->getPdo();
}
catch (Throwable $ex) {
return false;
}
return true;
}
/**
* setLocale for the application.
* @param string $locale
* @return void
*/
public function setLocale($locale)
{
parent::setLocale($locale);
$this['events']->dispatch('locale.changed', [$locale]);
}
//
// Core registrations
//
/**
* registerConfiguredProviders is entirely inherited from the parent,
* except the October\Rain namespace is included in the partition.
*/
public function registerConfiguredProviders()
{
$providers = (new Collection($this->make('config')->get('app.providers')))
->partition(function ($provider) {
return strpos($provider, 'Illuminate\\') === 0 ||
strpos($provider, 'October\\Rain\\') === 0;
});
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
$this->fireAppCallbacks($this->registeredCallbacks);
}
/**
* registerCoreContainerAliases in the container.
*/
public function registerCoreContainerAliases()
{
$aliases = [
// These are introduced externally, for example, RainLab.User plugin
// 'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
// 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
'app' => [\October\Rain\Foundation\Application::class, \Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class, \Psr\SimpleCache\CacheInterface::class],
'cache.psr6' => [\Symfony\Component\Cache\Adapter\Psr16Adapter::class, \Symfony\Component\Cache\Adapter\AdapterInterface::class, \Psr\Cache\CacheItemPoolInterface::class],
'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
'db.schema' => [\Illuminate\Database\Schema\Builder::class],
'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\StringEncrypter::class],
'events' => [\October\Rain\Events\Dispatcher::class, \Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
'files' => [\October\Rain\Filesystem\Filesystem::class, \Illuminate\Filesystem\Filesystem::class],
'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class],
'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class],
'hash' => [\Illuminate\Hashing\HashManager::class],
'hash.driver' => [\Illuminate\Contracts\Hashing\Hasher::class],
'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
'log' => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class],
'mail.manager' => [\Illuminate\Mail\MailManager::class, \Illuminate\Contracts\Mail\Factory::class],
'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
'auth.password' => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class],
'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
'redirect' => [\October\Rain\Router\CoreRedirector::class, \Illuminate\Routing\Redirector::class],
'redis' => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
'redis.connection' => [\Illuminate\Redis\Connections\Connection::class, \Illuminate\Contracts\Redis\Connection::class],
'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
'session' => [\Illuminate\Session\SessionManager::class],
'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
'validator' => [\October\Rain\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
];
foreach ($aliases as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
/**
* registerClassAlias registers a new global alias, useful for facades
*/
public function registerClassAlias(string $alias, string $class)
{
$this->registerClassAliases([$alias => $class]);
}
/**
* registerClassAliases registers multiple global aliases, useful for renamed classes
*/
public function registerClassAliases(array $aliases)
{
AliasLoader::getInstance($aliases);
}
//
// Caching
//
/**
* Get the path to the configuration cache file.
*
* @return string
*/
public function getCachedConfigPath()
{
return $this->normalizeCachePath('APP_CONFIG_CACHE', 'framework/config.php');
}
/**
* Get the path to the routes cache file.
*
* @return string
*/
public function getCachedRoutesPath()
{
return $this->normalizeCachePath('APP_ROUTES_CACHE', 'framework/routes.php');
}
/**
* Get the path to the cached "compiled.php" file.
*
* @return string
*/
public function getCachedCompilePath()
{
return $this->normalizeCachePath('APP_COMPILED_CACHE', 'framework/compiled.php');
}
/**
* Get the path to the cached services.json file.
*
* @return string
*/
public function getCachedServicesPath()
{
return $this->normalizeCachePath('APP_SERVICES_CACHE', 'framework/services.php');
}
/**
* Get the path to the cached packages.php file.
*
* @return string
*/
public function getCachedPackagesPath()
{
return $this->normalizeCachePath('APP_PACKAGES_CACHE', 'framework/packages.php');
}
/**
* Get the path to the cached packages.php file.
*
* @return string
*/
public function getCachedClassesPath()
{
return $this->normalizeCachePath('APP_CLASSES_CACHE', 'framework/classes.php');
}
/**
* Get the path to the events cache file.
*
* @return string
*/
public function getCachedEventsPath()
{
return $this->normalizeCachePath('APP_EVENTS_CACHE', 'framework/events.php');
}
/**
* getNamespace returns the application namespace.
* @return string
* @throws \RuntimeException
*/
public function getNamespace()
{
return 'App\\';
}
/**
* extendInstance is useful for extending singletons regardless of their execution
*/
public function extendInstance($abstract, Closure $callback)
{
$this->afterResolving($abstract, $callback);
if ($this->resolved($abstract)) {
$callback($this->make($abstract), $this);
}
}
}
================================================
FILE: src/Foundation/Bootstrap/HandleExceptions.php
================================================
getCachedConfigPath())) {
$items = require $cached;
$app->instance('config_loaded_from_cache', $loadedFromCache = true);
}
// Next we will spin through all of the configuration files in the configuration
// directory and load each one into the repository. This will make all of the
// options available to the developer for use in various parts of this app.
$app->instance('config', $config = new Repository($items));
if (!isset($loadedFromCache)) {
$this->loadConfigurationFiles($app, $config);
}
// Finally, we will set the application's environment based on the configuration
// values that were loaded. We will pass a callback which will be used to get
// the environment in a web context where an "--env" switch is not present.
$app->detectEnvironment(function () use ($config) {
return $config->get('app.env', 'production');
});
date_default_timezone_set($config->get('app.timezone', 'UTC'));
mb_internal_encoding('UTF-8');
// Fix for XDebug aborting threads > 100 nested
ini_set('xdebug.max_nesting_level', 1000);
}
/**
* loadConfigurationFiles from all of the files.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $repository
* @return void
*
* @throws \Exception
*/
protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)
{
$files = $this->getConfigurationFiles($app);
if (!isset($files['app'])) {
throw new Exception('Unable to load the "app" configuration file.');
}
foreach ($files as $key => $path) {
// Filenames with config.php are treated as root nodes
if (basename($path) === 'config.php') {
$key = substr($key, 0, -7);
}
$repository->set($key, require $path);
}
}
}
================================================
FILE: src/Foundation/Bootstrap/RegisterOctober.php
================================================
make('config')->set(
'app.providers',
$app->make('config')->get('app.providers') ?? ServiceProvider::defaultProviders()->toArray(),
);
// Register singletons
$app->singleton('string', function () {
return new \October\Rain\Support\Str;
});
// Change paths based on config
if ($storagePath = $app['config']->get('system.storage_path')) {
$app->useStoragePath($this->parseConfiguredPath($app, $storagePath));
}
if ($cachePath = $app['config']->get('system.cache_path')) {
$app->useCachePath($this->parseConfiguredPath($app, $cachePath));
}
if ($pluginsPath = $app['config']->get('system.plugins_path')) {
$app->usePluginsPath($this->parseConfiguredPath($app, $pluginsPath));
}
if ($themesPath = $app['config']->get('system.themes_path')) {
$app->useThemesPath($this->parseConfiguredPath($app, $themesPath));
}
// Make system paths
if ($app->cachePath() === $app->storagePath()) {
$this->makeSystemPaths($app->cachePath(), array_unique(
array_merge($this->cachePaths, $this->storagePaths)
));
}
else {
$this->makeSystemPaths($app->cachePath(), $this->cachePaths);
$this->makeSystemPaths($app->storagePath(), $this->storagePaths);
}
// Configure the custom class loader
$this->configureClassLoader($app);
// Clear service container
OctoberContainer::clearExtensions();
}
/**
* configureClassLoader initializes the class loader cache
*/
protected function configureClassLoader(Application $app)
{
$loader = ClassLoader::instance();
$loader->initManifest($app->getCachedClassesPath());
$app->after(function () use ($loader) {
$loader->build();
});
}
/**
* parseConfiguredPath will include the base path if necessary
*/
protected function parseConfiguredPath(Application $app, string $path): string
{
return Str::startsWith($path, '/')
? $path
: $app->basePath($path);
}
/**
* makeSystemPaths will attempt to ensure the required system paths exist
*/
protected function makeSystemPaths(string $rootPath, array $subPaths): void
{
if (file_exists($rootPath)) {
return;
}
@mkdir($rootPath);
foreach ($subPaths as $path) {
$subPath = $rootPath.DIRECTORY_SEPARATOR.$path;
if (file_exists($subPath)) {
continue;
}
@mkdir($subPath);
}
}
}
================================================
FILE: src/Foundation/Configuration/ApplicationBuilder.php
================================================
app->singleton(
\Illuminate\Contracts\Http\Kernel::class,
\October\Rain\Foundation\Http\Kernel::class
);
$this->app->singleton(
\Illuminate\Contracts\Console\Kernel::class,
\October\Rain\Foundation\Console\Kernel::class
);
return $this;
}
/**
* Register and configure the application's exception handler.
*
* @param callable|null $using
* @return $this
*/
public function withExceptions(?callable $using = null)
{
$this->app->singleton(
\Illuminate\Contracts\Debug\ExceptionHandler::class,
\October\Rain\Foundation\Exception\Handler::class
);
$using ??= fn () => true;
$this->app->afterResolving(
\Illuminate\Foundation\Exceptions\Handler::class,
fn ($handler) => $using(new Exceptions($handler)),
);
return $this;
}
/**
* withMiddleware is a modifier to the parent logic to remove unwanted default middleware
*/
public function withMiddleware(?callable $callback = null)
{
$nested = function($middleware) use ($callback) {
$middleware
->remove([
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
])
->append([
\October\Rain\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
])
->removeFromGroup('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
])
->appendToGroup('web', [
\October\Rain\Foundation\Http\Middleware\EncryptCookies::class,
]);
if ($callback !== null) {
$callback($middleware);
}
};
return parent::withMiddleware($nested);
}
}
================================================
FILE: src/Foundation/Console/ClearCompiledCommand.php
================================================
laravel->getCachedClassesPath())) {
@unlink($classesPath);
}
parent::handle();
}
}
================================================
FILE: src/Foundation/Console/Kernel.php
================================================
bootstrap();
$this->app['events']->dispatch('console.schedule', [$schedule]);
}
}
================================================
FILE: src/Foundation/Console/ProjectSetCommand.php
================================================
argument('key');
if (!$projectKey) {
$this->comment(__("Enter a valid License Key to proceed."));
$projectKey = trim($this->ask(__("License Key")));
}
try {
// Validate input with gateway
$result = $this->requestServerData('project/detail', ['id' => $projectKey]);
// Check project status
$isActive = $result['is_active'] ?? false;
if (!$isActive) {
$this->output->error(__("License is unpaid or has expired. Please visit octobercms.com to obtain a license."));
return;
}
// Store project details
$this->storeProjectDetails($result);
// Add gateway as a composer repo
ComposerManager::instance()->addOctoberRepository($this->getComposerUrl());
$this->output->success(__("Thanks for being a customer of October CMS!"));
}
catch (Exception $e) {
$this->output->error($e->getMessage());
return 1;
}
}
/**
* storeProjectDetails
*/
protected function storeProjectDetails($result)
{
// Save project locally
try {
if (class_exists(\System\Models\Parameter::class)) {
\System\Models\Parameter::set([
'system::project.id' => $result['id'],
'system::project.key' => $result['project_id'],
'system::project.name' => $result['name'],
'system::project.owner' => $result['owner'],
'system::project.is_active' => $result['is_active']
]);
}
else {
$this->storeProjectDetailsLocally($result);
}
}
catch (Exception $ex) {
$this->storeProjectDetailsLocally($result);
}
// Save authentication token
ComposerManager::instance()->addAuthCredentials(
$this->getComposerUrl(false),
$result['email'],
$result['project_id']
);
}
/**
* storeProjectDetailsLocally instead
*/
protected function storeProjectDetailsLocally($result)
{
if (!is_dir($cmsStorePath = storage_path('cms'))) {
mkdir($cmsStorePath);
}
$this->injectJsonToFile(storage_path('cms/project.json'), [
'project' => $result['project_id']
]);
}
/**
* requestServerData contacts the update server for a response.
* @param string $uri Gateway API URI
* @param array $postData Extra post data
* @return array
*/
public function requestServerData(string $uri, array $postData = [])
{
$result = $this->makeHttpRequest($this->createServerUrl($uri), $postData);
$contents = $result->body();
if ($result->status() === 404) {
throw new Exception(__('Response Not Found'));
}
if ($result->status() !== 200) {
throw new Exception(
strlen($contents)
? $contents
: __("Response Empty")
);
}
$resultData = false;
try {
$resultData = @json_decode($contents, true);
}
catch (Exception $ex) {
throw new Exception(__("Response Invalid"));
}
if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {
throw new Exception(__("Response Bad Format"));
}
return $resultData;
}
/**
* createServerUrl creates a complete gateway server URL from supplied URI
* @param string $uri URI
* @return string URL
*/
protected function createServerUrl($uri)
{
$gateway = Config::get('system.update_gateway', 'https://gateway.octobercms.com/api');
if (substr($gateway, -1) != '/') {
$gateway .= '/';
}
return $gateway . $uri;
}
/**
* makeHttpRequest makes a specialized server request to a URL.
* @param string $url
* @param array $postData
* @return \Illuminate\Http\Client\Response
*/
protected function makeHttpRequest($url, $postData)
{
// New HTTP instance
$http = Http::asForm();
// Post data
$postData['protocol_version'] = '2.0';
$postData['client'] = 'October CMS';
$postData['server'] = base64_encode(json_encode([
'php' => PHP_VERSION,
'url' => Url::to('/'),
'since' => date('c')
]));
// Gateway auth
if ($credentials = Config::get('system.update_gateway_auth')) {
if (is_string($credentials)) {
$credentials = explode(':', $credentials);
}
list($user, $pass) = $credentials;
$http->withBasicAuth($user, $pass);
}
return $http->post($url, $postData);
}
/**
* getComposerUrl returns the endpoint for composer
*/
protected function getComposerUrl(bool $withProtocol = true): string
{
$gateway = Config::get('system.composer_gateway', 'gateway.octobercms.com');
return $withProtocol ? 'https://'.$gateway : $gateway;
}
/**
* injectJsonToFile merges a JSON array in to an existing JSON file.
* Merging is useful for preserving array values.
*/
protected function injectJsonToFile(string $filename, array $jsonArr, bool $merge = false): void
{
$contentsArr = file_exists($filename)
? json_decode(file_get_contents($filename), true)
: [];
$newArr = $merge
? array_merge_recursive($contentsArr, $jsonArr)
: $this->mergeRecursive($contentsArr, $jsonArr);
$content = json_encode($newArr, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
file_put_contents($filename, $content);
}
/**
* mergeRecursive substitutes the native PHP array_merge_recursive to be
* more config friendly. Scalar values are replaced instead of being
* merged in to their own new array.
*/
protected function mergeRecursive(array $array1, $array2)
{
if ($array2 && is_array($array2)) {
foreach ($array2 as $key => $val2) {
if (
is_array($val2) &&
(($val1 = isset($array1[$key]) ? $array1[$key] : null) !== null) &&
is_array($val1)
) {
$array1[$key] = $this->mergeRecursive($val1, $val2);
}
else {
$array1[$key] = $val2;
}
}
}
return $array1;
}
}
================================================
FILE: src/Foundation/Console/RouteCacheCommand.php
================================================
getFreshApplication()['router']->registerLateRoutes();
return tap($routes->getRoutes(), function ($routes) {
$routes->refreshNameLookups();
$routes->refreshActionLookups();
});
}
}
================================================
FILE: src/Foundation/Console/RouteListCommand.php
================================================
registerLateRoutes();
}
parent::__construct($router);
}
}
================================================
FILE: src/Foundation/Console/ServeCommand.php
================================================
line("October CMS development server started: http://{$this->host()}:{$this->port()}");
$environmentFile = $this->option('env')
? base_path('.env').'.'.$this->option('env')
: base_path('.env');
$hasEnvironment = file_exists($environmentFile);
$environmentLastModified = $hasEnvironment
? filemtime($environmentFile)
: now()->addDays(30)->getTimestamp();
$process = $this->startProcess($hasEnvironment);
while ($process->isRunning()) {
if ($hasEnvironment) {
clearstatcache(false, $environmentFile);
}
if (! $this->option('no-reload') &&
$hasEnvironment &&
filemtime($environmentFile) > $environmentLastModified) {
$environmentLastModified = filemtime($environmentFile);
$this->comment('Environment modified. Restarting server...');
$process->stop(5);
$process = $this->startProcess($hasEnvironment);
}
usleep(500 * 1000);
}
$status = $process->getExitCode();
if ($status && $this->canTryAnotherPort()) {
$this->portOffset += 1;
return $this->handle();
}
return $status;
}
/**
* serverCommand gets the full server command.
* @return array
*/
protected function serverCommand()
{
$server = file_exists(base_path('server.php'))
? base_path('server.php')
: __DIR__.'/../resources/server.php';
return [
(new PhpExecutableFinder)->find(false),
'-S',
$this->host().':'.$this->port(),
$server,
];
}
}
================================================
FILE: src/Foundation/Exception/Handler.php
================================================
hasBootedEvents()) {
return;
}
/**
* @event exception.beforeReport
* Fires before the exception has been reported
*
* Example usage (prevents the reporting of a given exception)
*
* Event::listen('exception.beforeReport', function (\Exception $exception) {
* if ($exception instanceof \My\Custom\Exception) {
* return false;
* }
* });
*/
if (Event::fire('exception.beforeReport', [$exception], true) === false) {
return;
}
parent::report($exception);
/**
* @event exception.report
* Fired after the exception has been reported
*
* Example usage (performs additional reporting on the exception)
*
* Event::listen('exception.report', function (\Exception $exception) {
* App::make('sentry')->captureException($exception);
* });
*/
Event::fire('exception.report', [$exception]);
}
/**
* render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Throwable $exception)
{
// Exception occurred before system has booted
if (!$this->hasBootedEvents()) {
return parent::render($request, $exception);
}
$exception = $this->mapException($exception);
// Exception has a render method (Laravel 12)
if (method_exists($exception, 'render') && $response = $exception->render($request)) {
return $this->finalizeRenderedResponse(
$request,
Router::toResponse($request, $response),
$exception
);
}
// Exception wants to return its own response
if ($exception instanceof Responsable) {
return $this->finalizeRenderedResponse($request, $exception->toResponse($request), $exception);
}
// Convert to public-friendly exception
$exception = $this->prepareException($exception);
// Custom handlers
if ($response = $this->renderViaCallbacks($request, $exception)) {
return $this->finalizeRenderedResponse($request, $response, $exception);
}
// Exception is a response
if ($exception instanceof HttpResponseException) {
return $this->finalizeRenderedResponse($request, $exception->getResponse(), $exception);
}
/**
* @event exception.beforeRender
* Fires as the exception renders and returns an optional custom response.
*
* Example usage
*
* Event::listen('exception.beforeRender', function (\Exception $exception) {
* return 'An error happened!';
* });
*/
$statusCode = $this->getStatusCode($exception);
if (($event = Event::fire('exception.beforeRender', [$exception, $statusCode, $request], true)) !== null) {
return $this->finalizeRenderedResponse(
$request,
Response::make($event, $statusCode),
$exception
);
}
// Standard Laravel 12 rendering
return $this->finalizeRenderedResponse($request, match (true) {
$exception instanceof AuthenticationException => $this->unauthenticated($request, $exception),
$exception instanceof ValidationException => $this->convertValidationExceptionToResponse($exception, $request),
default => $this->renderExceptionResponse($request, $exception),
}, $exception);
}
/**
* prepareException for rendering.
*
* @param \Throwable $e
* @return \Throwable
*/
protected function prepareException(Throwable $e): Throwable
{
return match (true) {
// October-specific: NotFoundException → NotFoundHttpException
$e instanceof NotFoundException => new NotFoundHttpException($e->getMessage(), $e),
// Laravel 12 standard conversions
$e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
$e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
$e instanceof AuthorizationException && $e->hasStatus() => new HttpException(
$e->status(), $e->response()?->message() ?: (HttpResponse::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e
),
$e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e),
$e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),
$e instanceof RequestExceptionInterface => new BadRequestHttpException('Bad request.', $e),
$e instanceof RecordNotFoundException => new NotFoundHttpException('Not found.', $e),
$e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e),
default => $e,
};
}
/**
* getStatusCode checks if the exception implements the HttpExceptionInterface, or returns
* as generic 500 error code for a server side error.
* @param \Exception $exception
* @return int
*/
protected function getStatusCode($exception)
{
if ($exception instanceof HttpExceptionInterface) {
$code = $exception->getStatusCode();
}
elseif ($exception instanceof ForbiddenException) {
$code = 403;
}
elseif ($exception instanceof NotFoundHttpException) {
$code = 404;
}
elseif ($exception instanceof AjaxException) {
$code = 406;
}
else {
$code = 500;
}
return $code;
}
/**
* context is the the default context variables for logging.
*
* @return array
*/
protected function context()
{
return [];
}
//
// Custom handlers
//
/**
* @deprecated use renderable
*/
public function error(callable $callback)
{
$this->renderable($callback);
}
/**
* hasBootedEvents checks if we can broadcast events
*/
protected function hasBootedEvents(): bool
{
if (!class_exists('Event')) {
return false;
}
if (!$app = Event::getFacadeApplication()) {
return false;
}
if (!$app->bound('events')) {
return false;
}
return true;
}
}
================================================
FILE: src/Foundation/Http/Kernel.php
================================================
app->maintenanceMode()->data();
return Response::make(
View::make($view, [
'message' => $ex->getMessage(),
'retryAfter' => $data['retry'] ?? null,
]),
$ex->getStatusCode(),
$ex->getHeaders()
);
}
}
}
================================================
FILE: src/Foundation/Http/Middleware/EncryptCookies.php
================================================
disableFor($except);
}
}
================================================
FILE: src/Foundation/Providers/AppServiceProvider.php
================================================
app->singleton('october.installer', \October\Rain\Installer\InstallManager::class);
$this->registerConsoleCommand('october.build', \October\Rain\Installer\Console\OctoberBuild::class);
$this->registerConsoleCommand('october.install', \October\Rain\Installer\Console\OctoberInstall::class);
}
/**
* registerConsoleCommand registers a new console (artisan) command
*/
protected function registerConsoleCommand(string $key, string $class)
{
$key = 'command.'.$key;
$this->app->singleton($key, function ($app) use ($class) {
return $this->app->make($class);
});
$this->commands($key);
}
/**
* provides the returned services.
* @return array
*/
public function provides()
{
return [
'october.installer',
'command.october.build',
'command.october.install',
];
}
}
================================================
FILE: src/Foundation/Providers/ArtisanServiceProvider.php
================================================
RouteListCommand::class,
'RouteCache' => RouteCacheCommand::class,
'ProjectSet' => ProjectSetCommand::class,
'ClearCompiled' => ClearCompiledCommand::class,
];
/**
* @var array devCommands to be registered.
*/
protected $devCommandsRain = [
'Serve' => ServeCommand::class,
];
/**
* register the service provider
*/
public function register()
{
$this->registerCommands(array_merge(
$this->commands,
$this->commandsRain,
$this->devCommands,
$this->devCommandsRain
));
Signals::resolveAvailabilityUsing(function () {
return $this->app->runningInConsole()
&& ! $this->app->runningUnitTests()
&& extension_loaded('pcntl');
});
}
/**
* registerRouteCacheCommand
*/
protected function registerRouteCacheCommand()
{
$this->app->singleton(RouteCacheCommand::class, function ($app) {
return new RouteCacheCommand($app['files']);
});
}
/**
* registerRouteListCommand
*/
protected function registerRouteListCommand()
{
$this->app->singleton(RouteListCommand::class, function ($app) {
return new RouteListCommand($app['router']);
});
}
/**
* registerServeCommand
*/
protected function registerServeCommand()
{
$this->app->singleton(ServeCommand::class, function () {
return new ServeCommand;
});
}
/**
* registerClearCompiledCommand
*/
protected function registerClearCompiledCommand()
{
$this->app->singleton(ClearCompiledCommand::class, function () {
return new ClearCompiledCommand;
});
}
/**
* registerProjectSetCommand
*/
protected function registerProjectSetCommand()
{
$this->app->singleton(ProjectSetCommand::class, function () {
return new ProjectSetCommand;
});
}
}
================================================
FILE: src/Foundation/Providers/ConsoleSupportServiceProvider.php
================================================
app->singleton('core.composer', \October\Rain\Composer\ComposerManager::class);
}
/**
* provides the returned services.
* @return array
*/
public function provides()
{
return [
'core.composer',
];
}
}
================================================
FILE: src/Foundation/Providers/DateServiceProvider.php
================================================
app['config']->get('app.locale');
$this->setCarbonLocale($locale);
$this->app['events']->listen('locale.changed', function ($locale) {
$this->setCarbonLocale($locale);
});
}
/**
* setCarbonLocale sets the locale using the correct load order.
*/
protected function setCarbonLocale($locale)
{
Carbon::setLocale($locale);
CarbonImmutable::setLocale($locale);
CarbonPeriod::setLocale($locale);
CarbonInterval::setLocale($locale);
$fallbackLocale = $this->getFallbackLocale($locale);
if ($locale !== $fallbackLocale) {
Carbon::setFallbackLocale($fallbackLocale);
}
}
/**
* Split the locale and use it as the fallback.
*/
protected function getFallbackLocale($locale)
{
if ($position = strpos($locale, '-')) {
$target = substr($locale, 0, $position);
$resource = __DIR__ . '/../../../../nesbot/carbon/src/Carbon/Lang/'.$target.'.php';
if (file_exists($resource)) {
return $target;
}
}
return $this->app['config']->get('app.fallback_locale');
}
}
================================================
FILE: src/Foundation/Providers/ExecutionContextProvider.php
================================================
app->scoped('execution.context', function ($app) {
return $this->determineContext($app);
});
}
/**
* boot the service provider.
*/
public function boot()
{
// Refresh execution context when Octane receives a new request
if (class_exists(\Laravel\Octane\Events\RequestReceived::class)) {
$this->app['events']->listen(\Laravel\Octane\Events\RequestReceived::class, function ($event) {
$event->sandbox->forgetInstance('execution.context');
});
}
}
/**
* determineContext evaluates the execution context from the current request.
*/
protected function determineContext($app): string
{
$requestPath = $this->normalizeUrl($app['request']->path());
$backendUri = $this->normalizeUrl($app['config']->get('backend.uri', 'backend'));
if (str_starts_with($requestPath, $backendUri)) {
return 'backend';
}
return 'frontend';
}
/**
* normalizeUrl adds leading slash from a URL.
*
* @param string $url URL to normalize.
* @return string Returns normalized URL.
*/
protected function normalizeUrl($url)
{
if (substr($url, 0, 1) !== '/') {
$url = '/'.$url;
}
if (!strlen($url)) {
$url = '/';
}
return $url;
}
}
================================================
FILE: src/Foundation/Providers/LogServiceProvider.php
================================================
app->booting(function () {
$this->configureDefaultLogger($this->app['config']);
$this->configureDefaultPermissions($this->app['config'], $this->app['files']);
});
}
/**
* configureDefaultLogger channel for the application
* when no configuration is supplied.
*/
protected function configureDefaultLogger($config)
{
if ($config->get('logging.default', null) !== null) {
return;
}
// Set default values as single log file
$config->set('logging.default', 'single');
$config->set('logging.channels.single', [
'driver' => 'single',
'path' => storage_path('logs/system.log'),
'level' => 'debug',
]);
}
/**
* configureDefaultPermissions
*/
protected function configureDefaultPermissions($config, $files)
{
if ($config->get('logging.channels.single.permission', null) === null) {
$config->set('logging.channels.single.permission', $files->getFilePermissions());
}
if ($config->get('logging.channels.daily.permission', null) === null) {
$config->set('logging.channels.daily.permission', $files->getFilePermissions());
}
}
}
================================================
FILE: src/Foundation/resources/server.php
================================================
datasource = $datasource;
$this->processor = $processor;
}
/**
* whereFileName switches mode to select a single template by its name
* @param string $fileName
* @return $this
*/
public function whereFileName($fileName)
{
$this->selectSingle = $this->model->getFileNameParts($fileName);
return $this;
}
/**
* from sets the directory name which the query is targeting
* @param string $dirName
* @return $this
*/
public function from($dirName)
{
$this->from = $dirName;
return $this;
}
/**
* offset sets the "offset" value of the query
* @param int $value
* @return $this
*/
public function offset($value)
{
$this->offset = max(0, $value);
return $this;
}
/**
* skip is an alias to set the "offset" value of the query
* @param int $value
* @return \October\Rain\Halcyon\Builder|static
*/
public function skip($value)
{
return $this->offset($value);
}
/**
* limit sets the "limit" value of the query
* @param int $value
* @return $this
*/
public function limit($value)
{
if ($value >= 0) {
$this->limit = $value;
}
return $this;
}
/**
* take is an alias to set the "limit" value of the query
* @param int $value
* @return \October\Rain\Halcyon\Builder|static
*/
public function take($value)
{
return $this->limit($value);
}
/**
* find a single template by its file name.
* @param string $fileName
* @return mixed|static
*/
public function find($fileName)
{
return $this->whereFileName($fileName)->first();
}
/**
* first executes the query and get the first result
* @return mixed|static
*/
public function first()
{
return $this->limit(1)->get()->first();
}
/**
* get executes the query as a "select" statement
* @param array $columns
* @return \October\Rain\Halcyon\Collection|static[]
*/
public function get($columns = ['*'])
{
if (!is_null($this->cacheMinutes)) {
$results = $this->getCached($columns);
}
else {
$results = $this->getFresh($columns);
}
$models = $this->getModels($results ?: []);
return $this->model->newCollection($models);
}
/**
* pluck gets an array with the values of a given column
* @param string $column
* @param string $key
* @return Collection
*/
public function pluck($column, $key = null)
{
$select = is_null($key) ? [$column] : [$column, $key];
if (!is_null($this->cacheMinutes)) {
$results = $this->getCached($select);
}
else {
$results = $this->getFresh($select);
}
$collection = new Collection($results);
return $collection->pluck($column, $key);
}
/**
* lists is short-hand for pluck()->all()
* @param string $column
* @param string $key
* @return array
*/
public function lists($column, $key = null)
{
return $this->pluck($column, $key)->all();
}
/**
* getFresh executes the query as a fresh "select" statement
* @param array $columns
* @return \October\Rain\Halcyon\Collection|static[]
*/
public function getFresh($columns = ['*'])
{
if (is_null($this->columns)) {
$this->columns = $columns;
}
$processCmd = $this->selectSingle ? 'processSelectOne' : 'processSelect';
return $this->processor->{$processCmd}($this, $this->runSelect());
}
/**
* runSelect runs the query as a "select" statement against the datasource
* @return array
*/
protected function runSelect()
{
if (!is_string($this->from)) {
throw new ApplicationException(sprintf("The from property is invalid, make sure that %s has a string value for its \$dirName property (use '' if not using directories)", get_class($this->model)));
}
$this->validateDirectoryName($this->from);
if ($this->selectSingle) {
list($name, $extension) = $this->selectSingle;
$this->validateFileName($name . '.' . $extension);
return $this->datasource->selectOne($this->from, $name, $extension);
}
return $this->datasource->select($this->from, [
'columns' => $this->columns,
'extensions' => $this->extensions
]);
}
/**
* setModel instance for the model being queried
* @return $this
*/
public function setModel(Model $model)
{
$this->model = $model;
$this->extensions = $this->model->getAllowedExtensions();
$this->from($this->model->getObjectTypeDirName());
return $this;
}
/**
* toCompiled gets the compiled file content representation of the query
* @return string
*/
public function toCompiled()
{
return $this->processor->processUpdate($this, []);
}
/**
* insert a new record into the datasource.
* @return bool
*/
public function insert(array $values)
{
if (empty($values)) {
return true;
}
$this->validateFileName();
list($name, $extension) = $this->model->getFileNameParts();
$result = $this->processor->processInsert($this, $values);
return $this->datasource->insert(
$this->model->getObjectTypeDirName(),
$name,
$extension,
$result
);
}
/**
* update a record in the datasource.
* @return int
*/
public function update(array $values)
{
$this->validateFileName();
list($name, $extension) = $this->model->getFileNameParts();
$result = $this->processor->processUpdate($this, $values);
$oldName = $oldExtension = null;
if ($this->model->isDirty('fileName')) {
list($oldName, $oldExtension) = $this->model->getFileNameParts(
$this->model->getOriginal('fileName')
);
}
return $this->datasource->update(
$this->model->getObjectTypeDirName(),
$name,
$extension,
$result,
$oldName,
$oldExtension
);
}
/**
* delete a record from the database.
* @param string $fileName
* @return int
*/
public function delete()
{
$this->validateFileName();
list($name, $extension) = $this->model->getFileNameParts();
return $this->datasource->delete(
$this->model->getObjectTypeDirName(),
$name,
$extension
);
}
/**
* lastModified returns the last modified time of the object
* @return int
*/
public function lastModified()
{
$this->validateFileName();
list($name, $extension) = $this->model->getFileNameParts();
return $this->datasource->lastModified(
$this->model->getObjectTypeDirName(),
$name,
$extension
);
}
/**
* getModels gets the hydrated models
* @param array $results
* @return \October\Rain\Halcyon\Model[]
*/
public function getModels(array $results)
{
$datasource = $this->model->getDatasourceName();
$models = $this->model->hydrate($results, $datasource);
/*
* Flag the models as loaded from cache, then reset the internal property.
*/
if ($this->loadedFromCache) {
$models->each(function ($model) {
$model->setLoadedFromCache($this->loadedFromCache);
});
$this->loadedFromCache = false;
}
return $models->all();
}
/**
* getModel instance being queried
* @return \October\Rain\Halcyon\Model
*/
public function getModel()
{
return $this->model;
}
//
// Validation (Hard)
//
/**
* validateFileName validates the supplied filename, extension and path
* @param string $fileName
*/
protected function validateFileName($fileName = null)
{
if ($fileName === null) {
$fileName = $this->model->fileName;
}
if (!strlen($fileName)) {
throw (new MissingFileNameException)->setModel($this->model);
}
if (!$this->validateFileNamePath($fileName, $this->model->getMaxNesting())) {
throw (new InvalidFileNameException)->setInvalidFileName($fileName);
}
$this->validateFileNameExtension($fileName, $this->model->getAllowedExtensions());
return true;
}
/**
* validateDirectoryName validates the supplied directory path
* @param string $dirName
*/
protected function validateDirectoryName($dirName = null)
{
if (!$this->validateFileNamePath($dirName, $this->model->getMaxNesting())) {
throw (new InvalidDirectoryNameException)->setInvalidDirectoryName($dirName);
}
return true;
}
/**
* validateFileNameExtension validates whether a file has an allowed extension
* @param string $fileName Specifies a path to validate
* @param array $allowedExtensions A list of allowed file extensions
* @return void
*/
protected function validateFileNameExtension($fileName, $allowedExtensions)
{
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if (strlen($extension) && !in_array($extension, $allowedExtensions)) {
throw (new InvalidExtensionException)
->setInvalidExtension($extension)
->setAllowedExtensions($allowedExtensions)
;
}
}
/**
* validateFileNamePath validates a template path
* Template directory and file names can contain only alphanumeric symbols, dashes and dots.
* @param string $filePath Specifies a path to validate
* @param int $maxNesting Specifies the maximum allowed nesting level
* @return void
*/
protected function validateFileNamePath($filePath, $maxNesting = 5)
{
if (strpos($filePath, '..') !== false) {
return false;
}
if (strpos($filePath, './') !== false || strpos($filePath, '//') !== false) {
return false;
}
$segments = explode('/', $filePath);
if ($maxNesting !== null && count($segments) > ($maxNesting + 1)) {
return false;
}
foreach ($segments as $segment) {
if (!$this->validateFileNamePattern($segment)) {
return false;
}
}
return true;
}
/**
* validateFileNamePattern validates a template file or directory name
* template file names can contain only alphanumeric symbols, dashes, underscores and dots.
* @param string $fileName Specifies a path to validate
* @return boolean Returns true if the file name is valid. Otherwise returns false.
*/
protected function validateFileNamePattern($fileName)
{
return preg_match('/^[a-z0-9\_\-\.\/]+$/i', $fileName) ? true : false;
}
//
// Caching
//
/**
* remember indicates that the query results should be cached
* @param \DateTime|int $minutes
* @param string $key
* @return $this
*/
public function remember($minutes, $key = null)
{
list($this->cacheMinutes, $this->cacheKey) = [$minutes, $key];
return $this;
}
/**
* rememberForever indicates that the query results should be cached forever
* @param string $key
* @return $this
*/
public function rememberForever($key = null)
{
return $this->remember(-1, $key);
}
/**
* cacheTags indicates that the results, if cached, should use the given cache tags
* @param array|mixed $cacheTags
* @return $this
*/
public function cacheTags($cacheTags)
{
$this->cacheTags = $cacheTags;
return $this;
}
/**
* cacheDriver indicate that the results, if cached, should use the given cache driver
* @param string $cacheDriver
* @return $this
*/
public function cacheDriver($cacheDriver)
{
$this->cacheDriver = $cacheDriver;
return $this;
}
/**
* getCached executes the query as a cached "select" statement
* @param array $columns
* @return array
*/
public function getCached($columns = ['*'])
{
if (is_null($this->columns)) {
$this->columns = $columns;
}
$key = $this->getCacheKey();
$minutes = $this->cacheMinutes;
$cache = $this->getCache();
$callback = $this->getCacheCallback($columns);
$result = $cache->get($key);
$isNewCache = $result === null;
// If this is an old cache record, we can check if the cache has been busted
// by comparing the modification times. If this is the case, forget the
// cache and then prompt a recycle of the results.
if (!$isNewCache && $this->isCacheBusted($result)) {
$cache->forget($key);
$isNewCache = true;
}
// If the "minutes" value is less than zero, we will use that as the indicator
// that the value should be remembered values should be stored indefinitely
// and if we have minutes we will use the typical remember function here.
if ($isNewCache) {
$result = $callback();
if ($minutes < 0) {
$cache->forever($key, $result);
}
else {
$expiresAt = Carbon::now()->addMinutes($minutes);
$cache->put($key, $result, $expiresAt);
}
}
$this->loadedFromCache = !$isNewCache;
return $result;
}
/**
* isCacheBusted returns true if the cache for the file is busted. This only applies
* to single record selection
* @param array $result
* @return bool
*/
protected function isCacheBusted($result)
{
if (!$this->selectSingle) {
return false;
}
$mtime = $result ? Arr::get(reset($result), 'mtime') : null;
list($name, $extension) = $this->selectSingle;
$currentMtime = $this->datasource->lastModified(
$this->from,
$name,
$extension
);
return $currentMtime !== $mtime;
}
/**
* getCache object with tags assigned, if applicable
* @return \Illuminate\Cache\CacheManager
*/
protected function getCache()
{
$cache = $this->model->getCacheManager()->driver($this->cacheDriver);
return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache;
}
/**
* getCacheKey gets a unique cache key for the complete query
* @return string
*/
public function getCacheKey()
{
return $this->cacheKey ?: $this->generateCacheKey();
}
/**
* generateCacheKey for the query
* @return string
*/
public function generateCacheKey()
{
$payload = [];
$payload[] = $this->selectSingle ? serialize($this->selectSingle) : '*';
$payload[] = $this->orders ? serialize($this->orders) : '*';
$payload[] = $this->columns ? serialize($this->columns) : '*';
$payload[] = $this->fileMatch;
$payload[] = $this->limit;
$payload[] = $this->offset;
return 'halcyon_'.$this->from.'_'.$this->datasource->makeCacheKey(implode('-', $payload));
}
/**
* getCacheCallback used when caching queries
* @param string $fileName
* @return \Closure
*/
protected function getCacheCallback($columns)
{
return function () use ($columns) {
return $this->processInitCacheData($this->getFresh($columns));
};
}
/**
* processInitCacheData initializes the cache data of each record
* @param array $data
* @return array
*/
protected function processInitCacheData($data)
{
if ($data) {
$model = get_class($this->model);
foreach ($data as &$record) {
$model::initCacheItem($record);
}
}
return $data;
}
/**
* __call handles dynamic method calls into the method
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$className = static::class;
throw new BadMethodCallException("Call to undefined method {$className}::{$method}()");
}
}
================================================
FILE: src/Halcyon/Collection.php
================================================
'beforeCreate',
'created' => 'afterCreate',
'saving' => 'beforeSave',
'saved' => 'afterSave',
'updating' => 'beforeUpdate',
'updated' => 'afterUpdate',
'deleting' => 'beforeDelete',
'deleted' => 'afterDelete',
'fetching' => 'beforeFetch',
'fetched' => 'afterFetch',
];
foreach ($nicerEvents as $eventMethod => $method) {
self::registerModelEvent($eventMethod, function ($model) use ($method) {
$model->fireEvent("model.{$method}");
return $model->$method();
});
}
// Boot event
$this->fireEvent('model.afterBoot');
$this->afterBoot();
static::$eventsBooted[static::class] = true;
}
/**
* initializeModelEvent is called every time the model is constructed.
*/
protected function initializeModelEvent()
{
$this->fireEvent('model.afterInit');
$this->afterInit();
}
/**
* flushEventListeners removes all of the event listeners for the model.
*/
public static function flushEventListeners()
{
if (!isset(static::$dispatcher)) {
return;
}
$instance = new static;
foreach ($instance->getObservableEvents() as $event) {
static::$dispatcher->forget("halcyon.{$event}: ".static::class);
}
static::$eventsBooted = [];
}
/**
* getObservableEvents names.
* @return array
*/
public function getObservableEvents()
{
return array_merge(
[
'creating', 'created', 'updating', 'updated',
'deleting', 'deleted', 'saving', 'saved',
'fetching', 'fetched'
],
$this->observables
);
}
/**
* setObservableEvents names.
* @param array $observables
* @return $this
*/
public function setObservableEvents(array $observables)
{
$this->observables = $observables;
return $this;
}
/**
* addObservableEvents name.
* @param array|mixed $observables
* @return void
*/
public function addObservableEvents($observables)
{
$observables = is_array($observables) ? $observables : func_get_args();
$this->observables = array_unique(array_merge($this->observables, $observables));
}
/**
* removeObservableEvents name.
* @param array|mixed $observables
* @return void
*/
public function removeObservableEvents($observables)
{
$observables = is_array($observables) ? $observables : func_get_args();
$this->observables = array_diff($this->observables, $observables);
}
/**
* getEventDispatcher instance.
* @return \Illuminate\Contracts\Events\Dispatcher
*/
public static function getEventDispatcher()
{
return static::$dispatcher;
}
/**
* setEventDispatcher instance.
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public static function setEventDispatcher(Dispatcher $dispatcher)
{
static::$dispatcher = $dispatcher;
}
/**
* unsetEventDispatcher for models.
* @return void
*/
public static function unsetEventDispatcher()
{
static::$dispatcher = null;
}
/**
* registerModelEvent with the dispatcher.
* @param string $event
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
protected static function registerModelEvent($event, $callback, $priority = 0)
{
if (isset(static::$dispatcher)) {
$name = static::class;
static::$dispatcher->listen("halcyon.{$event}: {$name}", $callback, $priority);
}
}
/**
* fireModelEvent for the model.
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent($event, $halt = true)
{
if (!isset(static::$dispatcher)) {
return true;
}
// We will append the names of the class to the event to distinguish it from
// other model events that are fired, allowing us to listen on each model
// event set individually instead of catching event for all the models.
$event = "halcyon.{$event}: ".static::class;
$method = $halt ? 'until' : 'dispatch';
return static::$dispatcher->$method($event, $this);
}
/**
* Create a new native event for handling beforeFetch().
* @param Closure|string $callback
* @return void
*/
public static function fetching($callback)
{
static::registerModelEvent('fetching', $callback);
}
/**
* Create a new native event for handling afterFetch().
* @param Closure|string $callback
* @return void
*/
public static function fetched($callback)
{
static::registerModelEvent('fetched', $callback);
}
/**
* Register a saving model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function saving($callback, $priority = 0)
{
static::registerModelEvent('saving', $callback, $priority);
}
/**
* Register a saved model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function saved($callback, $priority = 0)
{
static::registerModelEvent('saved', $callback, $priority);
}
/**
* Register an updating model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function updating($callback, $priority = 0)
{
static::registerModelEvent('updating', $callback, $priority);
}
/**
* Register an updated model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function updated($callback, $priority = 0)
{
static::registerModelEvent('updated', $callback, $priority);
}
/**
* Register a creating model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function creating($callback, $priority = 0)
{
static::registerModelEvent('creating', $callback, $priority);
}
/**
* Register a created model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function created($callback, $priority = 0)
{
static::registerModelEvent('created', $callback, $priority);
}
/**
* Register a deleting model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function deleting($callback, $priority = 0)
{
static::registerModelEvent('deleting', $callback, $priority);
}
/**
* Register a deleted model event with the dispatcher.
*
* @param \Closure|string $callback
* @param int $priority
* @return void
*/
public static function deleted($callback, $priority = 0)
{
static::registerModelEvent('deleted', $callback, $priority);
}
/**
* afterBoot is called after the model is constructed for the first time.
*/
protected function afterBoot()
{
/**
* @event model.afterBoot
* Called after the model is booted
*
* Example usage:
*
* $model->bindEvent('model.afterBoot', function () use (\October\Rain\Halcyon\Model $model) {
* \Log::info(get_class($model) . ' has booted');
* });
*
*/
}
/**
* afterInit is called after the model is constructed, a nicer version
* of overriding the __construct method.
*/
protected function afterInit()
{
/**
* @event model.afterInit
* Called after the model is initialized
*
* Example usage:
*
* $model->bindEvent('model.afterInit', function () use (\October\Rain\Halcyon\Model $model) {
* \Log::info(get_class($model) . ' has initialized');
* });
*
*/
}
/**
* beforeCreate handles the "creating" model event
*/
protected function beforeCreate()
{
/**
* @event model.beforeCreate
* Called before the model is created
*
* Example usage:
*
* $model->bindEvent('model.beforeCreate', function () use (\October\Rain\Halcyon\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterCreate handles the "created" model event
*/
protected function afterCreate()
{
/**
* @event model.afterCreate
* Called after the model is created
*
* Example usage:
*
* $model->bindEvent('model.afterCreate', function () use (\October\Rain\Halcyon\Model $model) {
* \Log::info("{$model->name} was created!");
* });
*
*/
}
/**
* beforeUpdate handles the "updating" model event
*/
protected function beforeUpdate()
{
/**
* @event model.beforeUpdate
* Called before the model is updated
*
* Example usage:
*
* $model->bindEvent('model.beforeUpdate', function () use (\October\Rain\Halcyon\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterUpdate handles the "updated" model event
*/
protected function afterUpdate()
{
/**
* @event model.afterUpdate
* Called after the model is updated
*
* Example usage:
*
* $model->bindEvent('model.afterUpdate', function () use (\October\Rain\Halcyon\Model $model) {
* if ($model->title !== $model->original['title']) {
* \Log::info("{$model->name} updated its title!");
* }
* });
*
*/
}
/**
* beforeSave handles the "saving" model event
*/
protected function beforeSave()
{
/**
* @event model.beforeSave
* Called before the model is created or updated
*
* Example usage:
*
* $model->bindEvent('model.beforeSave', function () use (\October\Rain\Halcyon\Model $model) {
* if (!$model->isValid()) {
* throw new \Exception("Invalid Model!");
* }
* });
*
*/
}
/**
* afterSave handles the "saved" model event
*/
protected function afterSave()
{
/**
* @event model.afterSave
* Called after the model is created or updated
*
* Example usage:
*
* $model->bindEvent('model.afterSave', function () use (\October\Rain\Halcyon\Model $model) {
* if ($model->title !== $model->original['title']) {
* \Log::info("{$model->name} updated its title!");
* }
* });
*
*/
}
/**
* beforeDelete handles the "deleting" model event
*/
protected function beforeDelete()
{
/**
* @event model.beforeDelete
* Called before the model is deleted
*
* Example usage:
*
* $model->bindEvent('model.beforeDelete', function () use (\October\Rain\Halcyon\Model $model) {
* if (!$model->isAllowedToBeDeleted()) {
* throw new \Exception("You cannot delete me!");
* }
* });
*
*/
}
/**
* afterDelete handles the "deleted" model event
*/
protected function afterDelete()
{
/**
* @event model.afterDelete
* Called after the model is deleted
*
* Example usage:
*
* $model->bindEvent('model.afterDelete', function () use (\October\Rain\Halcyon\Model $model) {
* \Log::info("{$model->name} was deleted");
* });
*
*/
}
/**
* beforeFetch handles the "fetching" model event
*/
protected function beforeFetch()
{
/**
* @event model.beforeFetch
* Called before the model is fetched
*
* Example usage:
*
* $model->bindEvent('model.beforeFetch', function () use (\October\Rain\Halcyon\Model $model) {
* if (!\Auth::getUser()->hasAccess('fetch.this.model')) {
* throw new \Exception("You shall not pass!");
* }
* });
*
*/
}
/**
* afterFetch handles the "fetched" model event
*/
protected function afterFetch()
{
/**
* @event model.afterFetch
* Called after the model is fetched
*
* Example usage:
*
* $model->bindEvent('model.afterFetch', function () use (\October\Rain\Halcyon\Model $model) {
* \Log::info("{$model->name} was retrieved from the database");
* });
*
*/
}
}
================================================
FILE: src/Halcyon/Datasource/AutoDatasource.php
================================================
datasources = $datasources;
$this->primaryDatasource = Arr::first($datasources);
$this->postProcessor = new Processor;
}
/**
* hasTemplate checks if a template is found in the datasource
*/
public function hasTemplate(string $dirName, string $fileName, string $extension): bool
{
foreach ($this->datasources as $datasource) {
if ($datasource->hasTemplate($dirName, $fileName, $extension)) {
return true;
}
}
return false;
}
/**
* selectOne returns a single template
*/
public function selectOne(string $dirName, string $fileName, string $extension)
{
foreach ($this->datasources as $source) {
if (!$source->hasTemplate($dirName, $fileName, $extension)) {
continue;
}
return $source->selectOne($dirName, $fileName, $extension);
}
return null;
}
/**
* select returns all templates
*/
public function select(string $dirName, array $options = []): array
{
$result = [];
// Build from the ground up
foreach (array_reverse($this->datasources) as $datasource) {
$result = array_merge($result, $datasource->select($dirName, $options));
}
return collect($result)->keyBy('fileName')->all();
}
/**
* insert creates a new template
*/
public function insert(string $dirName, string $fileName, string $extension, string $content): bool
{
return $this->primaryDatasource->insert($dirName, $fileName, $extension, $content);
}
/**
* update an existing template
*/
public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int
{
$findFileName = $oldFileName ?: $fileName;
$findExt = $oldExtension ?: $extension;
if ($this->primaryDatasource->selectOne($dirName, $findFileName, $findExt)) {
$result = $this->primaryDatasource->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension);
}
else {
$result = $this->primaryDatasource->insert($dirName, $fileName, $extension, $content);
}
return $result;
}
/**
* delete against the datasource
*/
public function delete(string $dirName, string $fileName, string $extension): bool
{
return $this->primaryDatasource->delete($dirName, $fileName, $extension);
}
/**
* forceDelete against the datasource, forcing the complete removal of the template
*/
public function forceDelete(string $dirName, string $fileName, string $extension): bool
{
$result = false;
foreach ($this->datasources as $datasource) {
if ($datasource->delete($dirName, $fileName, $extension)) {
$result = true;
}
}
return $result;
}
/**
* lastModified returns the last modified date of an object
*/
public function lastModified(string $dirName, string $fileName, string $extension): ?int
{
foreach ($this->datasources as $source) {
if (!$source->hasTemplate($dirName, $fileName, $extension)) {
continue;
}
return $source->lastModified($dirName, $fileName, $extension);
}
return null;
}
/**
* makeCacheKey unique to this datasource
*/
public function makeCacheKey(string $name = ''): string
{
$key = '';
foreach ($this->datasources as $datasource) {
$key .= $datasource->makeCacheKey($name) . '-';
}
$key .= $name;
return (string) crc32($key);
}
//
// Models
//
/**
* hasIndex returns true if the specified index exists in datasources
*/
public function hasIndex(int $index)
{
return array_key_exists($index, $this->datasources);
}
/**
* hasModelAtIndex
*/
public function hasModelAtIndex($index, Model $model): bool
{
if (!$this->hasIndex($index)) {
return false;
}
// Get the path parts
$dirName = $model->getObjectTypeDirName();
list($fileName, $extension) = $model->getFileNameParts();
// Model doesn't exist
if ($fileName === null) {
return false;
}
return $this->datasources[$index]->hasTemplate($dirName, $fileName, $extension);
}
/**
* updateModelAtIndex updates at a specific datasource index
*/
public function updateModelAtIndex(int $index, Model $model): int
{
if (!$this->hasIndex($index)) {
return 0;
}
// Get the path parts
$dirName = $model->getObjectTypeDirName();
list($fileName, $extension) = $model->getFileNameParts();
$datasource = $this->datasources[$index];
// Get the file content
$content = $datasource->getPostProcessor()->processUpdate($model->newQuery(), []);
return $datasource->update($dirName, $fileName, $extension, $content);
}
/**
* deleteModelAtIndex against a specific datasource index
*/
public function forceDeleteModelAtIndex(int $index, Model $model): bool
{
if (!$this->hasIndex($index)) {
return false;
}
// Get the path parts
$dirName = $model->getObjectTypeDirName();
list($fileName, $extension) = $model->getFileNameParts();
return $this->datasources[$index]->forceDelete($dirName, $fileName, $extension);
}
}
================================================
FILE: src/Halcyon/Datasource/Datasource.php
================================================
postProcessor;
}
/**
* delete against the datasource
*/
public function delete(string $dirName, string $fileName, string $extension): bool
{
return true;
}
/**
* forceDelete a record against the datasource
*/
public function forceDelete(string $dirName, string $fileName, string $extension): bool
{
$this->forceDeleting = true;
$result = $this->delete($dirName, $fileName, $extension);
$this->forceDeleting = false;
return $result;
}
/**
* makeCacheKey unique to this datasource
*/
public function makeCacheKey(string $name = ''): string
{
return (string) crc32($name);
}
}
================================================
FILE: src/Halcyon/Datasource/DatasourceInterface.php
================================================
source = $source;
$this->table = $table;
$this->postProcessor = new Processor;
}
/**
* hasTemplate checks if a template is found in the datasource
*/
public function hasTemplate(string $dirName, string $fileName, string $extension): bool
{
return (bool) $this->lastModified($dirName, $fileName, $extension);
}
/**
* selectOne returns a single template
*/
public function selectOne(string $dirName, string $fileName, string $extension)
{
$path = $this->makeFilePath($dirName, $fileName, $extension);
if (isset(self::$pathCache[$this->source][$path])) {
$result = self::$pathCache[$this->source][$path];
}
else {
$result = $this->getQuery()->where('path', $path)->first();
}
if (!$result) {
return $result;
}
return [
'fileName' => $fileName . '.' . $extension,
'content' => $result->content,
'mtime' => Carbon::parse($result->updated_at)->timestamp,
'record' => $result
];
}
/**
* select returns all templates, with availableoptions:
*
* - columns: only return specific columns, eg: ['fileName', 'mtime', 'content']
* - extensions: extensions to search for, eg: ['htm', 'md', 'twig']
* - fileMatch: pattern to match the filename against using the fnmatch function, eg: *gr[ae]y
*/
public function select(string $dirName, array $options = []): array
{
$result = [];
extract(array_merge([
'columns' => null,
'extensions' => null,
'fileMatch' => null,
], $options));
if ($columns === ['*'] || !is_array($columns)) {
$columns = null;
}
// Apply the dirName query
$query = $this->getQuery()->where('path', 'like', $dirName . '%');
// Apply the extensions filter
if (is_array($extensions) && !empty($extensions)) {
$query->where(function ($query) use ($extensions) {
// Get the first extension to query for
$query->where('path', 'like', '%' . '.' . array_pop($extensions));
if (count($extensions)) {
foreach ($extensions as $ext) {
$query->orWhere('path', 'like', '%' . '.' . $ext);
}
}
});
}
// Retrieve the results
$results = $query->get();
foreach ($results as $item) {
self::$pathCache[$this->source][$item->path] = $item;
$resultItem = [];
$fileName = ltrim(str_replace($dirName, '', $item->path), '/');
// Apply the fileMatch filter
if (!empty($fileMatch) && !fnmatch($fileMatch, $fileName)) {
continue;
}
// Apply the columns filter on the data returned
if ($columns === null) {
$resultItem = [
'fileName' => $fileName,
'content' => $item->content,
'mtime' => Carbon::parse($item->updated_at)->timestamp,
'record' => $item,
];
}
else {
if (in_array('fileName', $columns)) {
$resultItem['fileName'] = $fileName;
}
if (in_array('content', $columns)) {
$resultItem['content'] = $item->content;
}
if (in_array('mtime', $columns)) {
$resultItem['mtime'] = Carbon::parse($item->updated_at)->timestamp;
}
if (in_array('record', $columns)) {
$resultItem['record'] = $item;
}
}
$result[] = $resultItem;
}
return $result;
}
/**
* insert creates a new template
*/
public function insert(string $dirName, string $fileName, string $extension, string $content): bool
{
$path = $this->makeFilePath($dirName, $fileName, $extension);
if ($this->getQuery()->where('path', $path)->count() > 0) {
throw (new FileExistsException())->setInvalidPath($path);
}
// Update a trashed record
if ($this->getQuery(false)->where('path', $path)->first()) {
return $this->update($dirName, $fileName, $extension, $content);
}
try {
$record = [
'source' => $this->source,
'path' => $path,
'content' => $content,
'file_size' => mb_strlen($content, '8bit'),
'updated_at' => Carbon::now()->toDateTimeString(),
'deleted_at' => null,
];
/**
* @event halcyon.datasource.db.beforeInsert
* Provides an opportunity to modify records before being inserted into the DB
*
* Example usage:
*
* $datasource->bindEvent('halcyon.datasource.db.beforeInsert', function ((array) &$record) {
* // Attach a site id to every record in a multi-tenant application
* $record['site_id'] = SiteManager::getSite()->id;
* });
*
*/
$this->fireEvent('halcyon.datasource.db.beforeInsert', [&$record]);
$this->getBaseQuery()->insert($record);
$this->flushCache();
return $record['file_size'];
}
catch (Exception $ex) {
throw (new CreateFileException)->setInvalidPath($path);
}
}
/**
* update an existing template
*/
public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int
{
$path = $this->makeFilePath($dirName, $fileName, $extension);
// Check if this file has been renamed
if ($oldFileName !== null) {
$fileName = $oldFileName;
}
if ($oldExtension !== null) {
$extension = $oldExtension;
}
$oldPath = $this->makeFilePath($dirName, $fileName, $extension);
try {
$fileSize = mb_strlen($content, '8bit');
$data = [
'path' => $path,
'content' => $content,
'file_size' => $fileSize,
'updated_at' => Carbon::now()->toDateTimeString(),
'deleted_at' => null
];
/**
* @event halcyon.datasource.db.beforeUpdate
* Provides an opportunity to modify records before being updated into the DB
*
* Example usage:
*
* $datasource->bindEvent('halcyon.datasource.db.beforeUpdate', function ((array) &$data) {
* // Attach a site id to every record in a multi-tenant application
* $data['site_id'] = SiteManager::getSite()->id;
* });
*
*/
$this->fireEvent('halcyon.datasource.db.beforeUpdate', [&$data]);
$this->getQuery(false)->where('path', $oldPath)->update($data);
$this->flushCache();
return $fileSize;
}
catch (Exception $ex) {
throw (new CreateFileException)->setInvalidPath($path);
}
}
/**
* delete against the datasource
*/
public function delete(string $dirName, string $fileName, string $extension): bool
{
try {
$path = $this->makeFilePath($dirName, $fileName, $extension);
$recordQuery = $this->getQuery()->where('path', $path);
if ($this->forceDeleting) {
$result = $recordQuery->delete();
}
else {
$result = $recordQuery->update(['deleted_at' => Carbon::now()->toDateTimeString()]);
}
$this->flushCache();
return (bool) $result;
}
catch (Exception $ex) {
throw (new DeleteFileException)->setInvalidPath($path);
}
}
/**
* lastModified date of an object
*/
public function lastModified(string $dirName, string $fileName, string $extension): ?int
{
try {
if (!isset(self::$mtimeCache[$this->source])) {
self::$mtimeCache[$this->source] = $this->getQuery()->pluck('updated_at', 'path')->all();
}
$path = $this->makeFilePath($dirName, $fileName, $extension);
if (!isset(self::$mtimeCache[$this->source][$path])) {
return null;
}
$result = self::$mtimeCache[$this->source][$path];
return Carbon::parse($result)->timestamp;
}
catch (Exception $ex) {
return null;
}
}
/**
* makeCacheKey unique to this datasource
*/
public function makeCacheKey(string $name = ''): string
{
return (string) crc32($this->source . $name);
}
/**
* getBaseQuery builder object
*/
protected function getBaseQuery()
{
return Db::table($this->table);
}
/**
* getQuery object
*/
protected function getQuery(bool $withTrashed = true)
{
$query = $this->getBaseQuery();
$query->where('source', $this->source);
if ($withTrashed) {
$query->whereNull('deleted_at');
}
/**
* @event halcyon.datasource.db.extendQuery
* Provides an opportunity to modify the query object used by the Halycon DbDatasource
*
* Example usage:
*
* $datasource->bindEvent('halcyon.datasource.db.extendQuery', function ((QueryBuilder) $query, (bool) $withTrashed) {
* // Apply a site filter in a multi-tenant application
* $query->where('site_id', SiteManager::getSite()->id);
* });
*
*/
$this->fireEvent('halcyon.datasource.db.extendQuery', [$query, $withTrashed]);
return $query;
}
/**
* makeFilePath helper to make file path
*/
protected function makeFilePath(string $dirName, string $fileName, string $extension): string
{
return $dirName . '/' . $fileName . '.' . $extension;
}
/**
* flushCache
*/
protected function flushCache()
{
unset(self::$pathCache[$this->source]);
unset(self::$mtimeCache[$this->source]);
}
}
================================================
FILE: src/Halcyon/Datasource/FileDatasource.php
================================================
basePath = $basePath;
$this->files = $files;
$this->postProcessor = new Processor;
}
/**
* hasTemplate checks if a template is found in the datasource
*/
public function hasTemplate(string $dirName, string $fileName, string $extension): bool
{
return (bool) $this->selectOne($dirName, $fileName, $extension);
}
/**
* selectOne returns a single template
*/
public function selectOne(string $dirName, string $fileName, string $extension)
{
try {
$path = $this->makeFilePath($dirName, $fileName, $extension);
return [
'fileName' => $fileName . '.' . $extension,
'content' => $this->files->get($path),
'mtime' => $this->files->lastModified($path)
];
}
catch (Exception $ex) {
return null;
}
}
/**
* select returns all templates, with available options:
*
* - columns: only return specific columns, eg: ['fileName', 'mtime', 'content']
* - extensions: extensions to search for, eg: ['htm', 'md', 'twig']
* - fileMatch: pattern to match the filename against using the fnmatch function, eg: *gr[ae]y
*/
public function select(string $dirName, array $options = []): array
{
extract(array_merge([
'columns' => null,
'extensions' => null,
'fileMatch' => null,
], $options));
$result = [];
$dirPath = $this->basePath . '/' . $dirName;
if (!$this->files->isDirectory($dirPath)) {
return $result;
}
if ($columns === ['*'] || !is_array($columns)) {
$columns = null;
}
else {
$columns = array_flip($columns);
}
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirPath));
// @todo This should come from $maxNesting defined in the model -sg
$it->setMaxDepth(5);
$it->rewind();
while ($it->valid()) {
if (!$it->isFile()) {
$it->next();
continue;
}
// Filter by extension
//
$fileExt = $it->getExtension();
if ($extensions !== null && !in_array($fileExt, $extensions)) {
$it->next();
continue;
}
$fileName = $it->getBasename();
if ($it->getDepth() > 0) {
$baseName = $this->files->normalizePath(substr($it->getPath(), strlen($dirPath) + 1));
$fileName = $baseName . '/' . $fileName;
}
// Filter by file name match
//
if ($fileMatch !== null && !fnmatch($fileMatch, $fileName)) {
$it->next();
continue;
}
$item = [];
$path = $this->basePath . '/' . $dirName . '/' . $fileName;
$item['fileName'] = $fileName;
if (!$columns || array_key_exists('content', $columns)) {
$item['content'] = $this->files->get($path);
}
if (!$columns || array_key_exists('mtime', $columns)) {
$item['mtime'] = $this->files->lastModified($path);
}
$result[] = $item;
$it->next();
}
return $result;
}
/**
* insert creates a new template
*/
public function insert(string $dirName, string $fileName, string $extension, string $content): bool
{
$this->validateDirectoryForSave($dirName, $fileName, $extension);
$path = $this->makeFilePath($dirName, $fileName, $extension);
if ($this->files->isFile($path)) {
throw (new FileExistsException)->setInvalidPath($path);
}
try {
return $this->files->put($path, $content);
}
catch (Exception $ex) {
throw (new CreateFileException)->setInvalidPath($path);
}
}
/**
* update an existing template
*/
public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int
{
$this->validateDirectoryForSave($dirName, $fileName, $extension);
$path = $this->makeFilePath($dirName, $fileName, $extension);
// The same file is safe to rename when the case is changed
// eg: FooBar -> foobar
$iFileChanged = ($oldFileName !== null && strcasecmp($oldFileName, $fileName) !== 0) ||
($oldExtension !== null && strcasecmp($oldExtension, $extension) !== 0);
if ($iFileChanged && $this->files->isFile($path)) {
throw (new FileExistsException)->setInvalidPath($path);
}
// File to be renamed, as delete and recreate
$fileChanged = ($oldFileName !== null && strcmp($oldFileName, $fileName) !== 0) ||
($oldExtension !== null && strcmp($oldExtension, $extension) !== 0);
if ($fileChanged) {
$this->delete($dirName, $oldFileName, $oldExtension);
}
try {
return $this->files->put($path, $content);
}
catch (Exception $ex) {
throw (new CreateFileException)->setInvalidPath($path);
}
}
/**
* delete against the datasource
*/
public function delete(string $dirName, string $fileName, string $extension): bool
{
$path = $this->makeFilePath($dirName, $fileName, $extension);
try {
return $this->files->delete($path);
}
catch (Exception $ex) {
throw (new DeleteFileException)->setInvalidPath($path);
}
}
/**
* lastModified date of an object
*/
public function lastModified(string $dirName, string $fileName, string $extension): ?int
{
try {
$path = $this->makeFilePath($dirName, $fileName, $extension);
return $this->files->lastModified($path);
}
catch (Exception $ex) {
return null;
}
}
/**
* validateDirectoryForSave ensures the requested file can be created in
* the requested directory
*/
protected function validateDirectoryForSave(string $dirName, string $fileName, string $extension)
{
$path = $this->makeFilePath($dirName, $fileName, $extension);
$dirPath = $this->basePath . '/' . $dirName;
// Create base directory
if (
(!$this->files->exists($dirPath) || !$this->files->isDirectory($dirPath)) &&
!$this->files->makeDirectory($dirPath, 0755, true, true)
) {
throw (new CreateDirectoryException)->setInvalidPath($dirPath);
}
// Create base file directory
if (($pos = strpos($fileName, '/')) !== false) {
$fileDirPath = dirname($path);
if (
!$this->files->isDirectory($fileDirPath) &&
!$this->files->makeDirectory($fileDirPath, 0755, true, true)
) {
throw (new CreateDirectoryException)->setInvalidPath($fileDirPath);
}
}
}
/**
* makeFilePath helper to make file path
*/
protected function makeFilePath(string $dirName, string $fileName, string $extension): string
{
return $this->basePath . '/' . $dirName . '/' .$fileName . '.' . $extension;
}
/**
* getBasePath returns the base path for this datasource
*/
public function getBasePath(): string
{
return $this->basePath;
}
/**
* makeCacheKey unique to this datasource
*/
public function makeCacheKey(string $name = ''): string
{
return crc32($this->basePath . $name);
}
}
================================================
FILE: src/Halcyon/Datasource/Resolver.php
================================================
$datasource) {
$this->addDatasource($name, $datasource);
}
}
/**
* datasource instance
*/
public function datasource(?string $name = null): DatasourceInterface
{
if ($name === null) {
$name = $this->getDefaultDatasource();
}
return $this->datasources[$name];
}
/**
* addDatasource to the resolver
*/
public function addDatasource(string $name, DatasourceInterface $datasource)
{
$this->datasources[$name] = $datasource;
}
/**
* hasDatasource checks if a datasource has been registered
*/
public function hasDatasource(string $name): bool
{
return isset($this->datasources[$name]);
}
/**
* getDefaultDatasource name
*/
public function getDefaultDatasource(): ?string
{
return $this->default;
}
/**
* setDefaultDatasource name
*/
public function setDefaultDatasource(string $name)
{
$this->default = $name;
}
}
================================================
FILE: src/Halcyon/Datasource/ResolverInterface.php
================================================
invalidPath = $path;
$this->message = "Error creating directory [{$path}]. Please check write permissions.";
return $this;
}
/**
* getInvalidPath is the affected directory path
*/
public function getInvalidPath(): string
{
return File::nicePath($this->invalidPath);
}
}
================================================
FILE: src/Halcyon/Exception/CreateFileException.php
================================================
invalidPath = $path;
$this->message = "Error creating file [{$path}]. Please check write permissions.";
return $this;
}
/**
* getInvalidPath is the affected directory path
*/
public function getInvalidPath(): string
{
return File::nicePath($this->invalidPath);
}
}
================================================
FILE: src/Halcyon/Exception/DeleteFileException.php
================================================
invalidPath = $path;
$this->message = "Error deleting file [{$path}]. Please check write permissions.";
return $this;
}
/**
* getInvalidPath is the affected directory path
*/
public function getInvalidPath(): string
{
return File::nicePath($this->invalidPath);
}
}
================================================
FILE: src/Halcyon/Exception/FileExistsException.php
================================================
invalidPath = $path;
$this->message = "A file already exists at [{$path}].";
return $this;
}
/**
* getInvalidPath is the affected directory path
*/
public function getInvalidPath(): string
{
return File::nicePath($this->invalidPath);
}
}
================================================
FILE: src/Halcyon/Exception/InvalidDirectoryNameException.php
================================================
invalidDirName = $invalidDirName;
$this->message = "The specified directory name [{$invalidDirName}] is invalid.";
return $this;
}
/**
* getInvalidDirectoryName gets the affected file name
*/
public function getInvalidDirectoryName(): string
{
return $this->invalidDirName;
}
}
================================================
FILE: src/Halcyon/Exception/InvalidExtensionException.php
================================================
invalidExtension = $invalidExtension;
$this->message = "The specified file extension [{$invalidExtension}] is invalid.";
return $this;
}
/**
* getInvalidExtension gets the affected file extension
*/
public function getInvalidExtension(): string
{
return $this->invalidExtension;
}
/**
* setAllowedExtensions sets the list of allowed extensions
*/
public function setAllowedExtensions(array $allowedExtensions): InvalidExtensionException
{
$this->allowedExtensions = $allowedExtensions;
return $this;
}
/**
* getAllowedExtensions gets the list of allowed extensions
*/
public function getAllowedExtensions(): array
{
return $this->allowedExtensions;
}
}
================================================
FILE: src/Halcyon/Exception/InvalidFileNameException.php
================================================
invalidFileName = $invalidFileName;
$this->message = "The specified file name [{$invalidFileName}] is invalid.";
return $this;
}
/**
* getInvalidFileName gets the affected file name
*/
public function getInvalidFileName(): string
{
return $this->invalidFileName;
}
}
================================================
FILE: src/Halcyon/Exception/MissingFileNameException.php
================================================
model = $model;
$this->message = "No file name attribute (fileName) specified for model [{$model}].";
return $this;
}
/**
* getModel gets the affected Halcyon model
*/
public function getModel(): string
{
return $this->model;
}
}
================================================
FILE: src/Halcyon/Exception/ModelException.php
================================================
model = $model;
$this->validationErrors = $model->errors();
// Bypass parent constructor to avoid Validator facade dependency
Exception::__construct($this->validationErrors->first());
$this->evalErrors();
}
/**
* errors returns validation errors
*/
public function errors(): array
{
return $this->validationErrors->messages();
}
/**
* getErrors returns the message bag instance
*/
public function getErrors(): MessageBag
{
return $this->validationErrors;
}
/**
* getModel returns the model with invalid attributes
*/
public function getModel(): Model
{
return $this->model;
}
}
================================================
FILE: src/Halcyon/HalcyonServiceProvider.php
================================================
app['halcyon']);
Model::setEventDispatcher($this->app['events']);
Model::setCacheManager($this->app['cache']);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
Model::clearBootedModels();
Model::flushEventListeners();
$this->app->singleton('halcyon', function ($app) {
return new Resolver;
});
}
}
================================================
FILE: src/Halcyon/Migrations/2021_10_01_000001_Db_Templates.php
================================================
increments('id');
$table->string('source')->index();
$table->string('path')->index();
$table->longText('content');
$table->integer('file_size')->unsigned();
$table->dateTime('updated_at')->nullable();
$table->dateTime('deleted_at')->nullable();
});
}
public function down()
{
Schema::dropIfExists('templates');
}
};
================================================
FILE: src/Halcyon/Model.php
================================================
bootIfNotBooted();
$this->initializeTraits();
$this->bootNicerEvents();
parent::__construct();
$this->initializeModelEvent();
$this->syncOriginal();
$this->fill($attributes);
}
/**
* bootIfNotBooted checks if the model needs to be booted and if so, do it.
*/
protected function bootIfNotBooted()
{
if (!isset(static::$booted[static::class])) {
static::$booted[static::class] = true;
$this->fireModelEvent('booting', false);
static::booting();
static::boot();
static::booted();
$this->fireModelEvent('booted', false);
}
}
/**
* booting performs any actions required before the model boots.
*/
protected static function booting()
{
//
}
/**
* boot is the "booting" method of the model.
*/
protected static function boot()
{
static::bootTraits();
}
/**
* bootTraits boots all of the bootable traits on the model.
*/
protected static function bootTraits()
{
$class = static::class;
$booted = [];
static::$traitInitializers[$class] = [];
foreach (class_uses_recursive($class) as $trait) {
$method = 'boot'.class_basename($trait);
if (method_exists($class, $method) && ! in_array($method, $booted)) {
forward_static_call([$class, $method]);
$booted[] = $method;
}
if (method_exists($class, $method = 'initialize'.class_basename($trait))) {
static::$traitInitializers[$class][] = $method;
static::$traitInitializers[$class] = array_unique(
static::$traitInitializers[$class]
);
}
}
}
/**
* initializeTraits on the model.
*/
protected function initializeTraits()
{
foreach (static::$traitInitializers[static::class] as $method) {
$this->{$method}();
}
}
/**
* booted performs any actions required after the model boots.
*/
protected static function booted()
{
//
}
/**
* clearBootedModels clears the list of booted models so they will be re-booted.
*/
public static function clearBootedModels()
{
static::$booted = [];
}
/**
* getIdAttribute is a helper for {{ page.id }} or {{ layout.id }} twig vars
* Returns a semi-unique string for this object.
* @return string
*/
public function getIdAttribute()
{
return str_replace('/', '-', $this->getBaseFileNameAttribute());
}
/**
* getBaseFileNameAttribute returns the file name without the extension.
* @return string
*/
public function getBaseFileNameAttribute()
{
$pos = strrpos($this->fileName, '.');
if ($pos === false) {
return $this->fileName;
}
return substr($this->fileName, 0, $pos);
}
/**
* addFillable adds fillable attributes for the model.
* @param array|string|null $attributes
* @return void
*/
public function addFillable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->fillable = array_merge($this->fillable, $attributes);
}
/**
* addPurgeable adds an attribute to the purgeable attributes list
* @param array|string|null $attributes
* @return void
*/
public function addPurgeable($attributes = null)
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$this->purgeable = array_merge($this->purgeable, $attributes);
}
/**
* getSettingsAttribute is the settings is attribute contains everything that should
* be saved to the settings area.
* @return array
*/
public function getSettingsAttribute()
{
$defaults = [
'fileName',
'components',
'content',
'markup',
'mtime',
'code'
];
return array_diff_key(
$this->attributes,
array_flip(array_merge($defaults, $this->purgeable))
);
}
/**
* setSettingsAttribute filling the settings should merge it with attributes.
* @param mixed $value
*/
public function setSettingsAttribute($value)
{
if (is_array($value)) {
$this->attributes = array_merge($this->attributes, $value);
}
}
/**
* setFileNameAttribute wjere file name should always contain an extension.
* @param mixed $value
*/
public function setFileNameAttribute($value)
{
$fileName = trim($value);
if (strlen($fileName) && !strlen(pathinfo($value, PATHINFO_EXTENSION))) {
$fileName .= '.'.$this->defaultExtension;
}
$this->attributes['fileName'] = $fileName;
}
/**
* getObjectTypeDirName returns the directory name corresponding to the object type.
* For pages the directory name is "pages", for layouts - "layouts", etc.
* @return string
*/
public function getObjectTypeDirName()
{
return $this->dirName;
}
/**
* getAllowedExtensions returns the allowable file extensions supported by this model.
* @return array
*/
public function getAllowedExtensions()
{
return $this->allowedExtensions;
}
/**
* isCompoundObject returns true if this template supports code and settings sections.
* @return bool
*/
public function isCompoundObject()
{
return $this->isCompoundObject;
}
/**
* getWrapCode returns true if the code section will be wrapped in PHP tags.
* @return bool
*/
public function getWrapCode()
{
return $this->wrapCode;
}
/**
* getMaxNesting returns the maximum directory nesting allowed by this template.
* @return int
*/
public function getMaxNesting()
{
return $this->maxNesting;
}
/**
* isLoadedFromCache returns true if the object was loaded from the cache.
* @return boolean
*/
public function isLoadedFromCache()
{
return $this->loadedFromCache;
}
/**
* setLoadedFromCache returns true if the object was loaded from the cache.
* @return bool
*/
public function setLoadedFromCache($value)
{
$this->loadedFromCache = (bool) $value;
}
/**
* fill the model with an array of attributes.
* @param array $attributes
* @return $this
*/
public function fill(array $attributes)
{
foreach ($this->fillableFromArray($attributes) as $key => $value) {
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
}
}
return $this;
}
/**
* fillableFromArray gets the fillable attributes of a given array.
* @param array $attributes
* @return array
*/
protected function fillableFromArray(array $attributes)
{
$defaults = ['fileName'];
if (count($this->fillable) > 0) {
return array_intersect_key(
$attributes,
array_flip(array_merge($defaults, $this->fillable))
);
}
return $attributes;
}
/**
* newInstance creates a new instance of the given model.
* @param array $attributes
* @param bool $exists
* @return static
*/
public function newInstance($attributes = [], $exists = false)
{
// This method just provides a convenient way for us to generate fresh model
// instances of this current model. It is particularly useful during the
// hydration of new objects via the Halcyon query builder instances.
$model = new static((array) $attributes);
$model->exists = $exists;
return $model;
}
/**
* newFromBuilder creates a new model instance that is existing.
* @param array $attributes
* @param string|null $datasource
* @return static
*/
public function newFromBuilder($attributes = [], $datasource = null)
{
$instance = $this->newInstance([], true);
if ($instance->fireModelEvent('fetching') === false) {
return $instance;
}
$instance->setRawAttributes((array) $attributes, true);
$instance->fireModelEvent('fetched', false);
$instance->setDatasource($datasource ?: $this->datasource);
return $instance;
}
/**
* hydrate creates a collection of models from plain arrays.
* @param array $items
* @param string|null $datasource
* @return \October\Rain\Halcyon\Collection
*/
public static function hydrate(array $items, $datasource = null)
{
$instance = (new static)->setDatasource($datasource);
$items = array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item);
}, $items);
return $instance->newCollection($items);
}
/**
* create saves a new model and return the instance.
* @param array $attributes
* @return static
*/
public static function create(array $attributes = [])
{
$model = new static($attributes);
$model->save();
return $model;
}
/**
* query begins querying the model.
* @return \October\Rain\Halcyon\Builder
*/
public static function query()
{
return (new static)->newQuery();
}
/**
* on begins querying the model on a given datasource.
* @param string|null $datasource
* @return \October\Rain\Halcyon\Model
*/
public static function on($datasource = null)
{
// First we will just create a fresh instance of this model, and then we can
// set the datasource on the model so that it is be used for the queries.
$instance = new static;
$instance->setDatasource($datasource);
return $instance;
}
/**
* all of the models from the datasource.
* @return \October\Rain\Halcyon\Collection|static[]
*/
public static function all()
{
$instance = new static;
return $instance->newQuery()->get();
}
/**
* isFillable determines if the given attribute may be mass assigned.
* @param string $key
* @return bool
*/
public function isFillable($key)
{
// File name is always treated as a fillable attribute.
if ($key === 'fileName') {
return true;
}
// If the key is in the "fillable" array, we can of course assume that it's
// a fillable attribute. Otherwise, we will check the guarded array when
// we need to determine if the attribute is black-listed on the model.
if (in_array($key, $this->fillable)) {
return true;
}
return empty($this->fillable) && !Str::startsWith($key, '_');
}
/**
* toJson converts the model instance to JSON.
* @param int $options
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->jsonSerialize(), $options);
}
/**
* jsonSerialize converts the object into something JSON serializable.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* toArray converts the model instance to an array.
* @return array
*/
public function toArray()
{
return $this->attributesToArray();
}
/**
* attributesToArray converts the model's attributes to an array.
* @return array
*/
public function attributesToArray()
{
$attributes = $this->attributes;
$mutatedAttributes = $this->getMutatedAttributes();
// We want to spin through all the mutated attributes for this model and call
// the mutator for the attribute. We cache off every mutated attributes so
// we don't have to constantly check on attributes that actually change.
foreach ($mutatedAttributes as $key) {
if (!array_key_exists($key, $attributes)) {
continue;
}
$attributes[$key] = $this->mutateAttributeForArray(
$key,
$attributes[$key]
);
}
// Here we will grab all of the appended, calculated attributes to this model
// as these attributes are not really in the attributes array, but are run
// when we need to array or JSON the model for convenience to the coder.
foreach ($this->getArrayableAppends() as $key) {
$attributes[$key] = $this->mutateAttributeForArray($key, null);
}
return $attributes;
}
/**
* getArrayableAppends gets all of the appendable values that are arrayable.
* @return array
*/
protected function getArrayableAppends()
{
$defaults = ['settings'];
if (!count($this->appends)) {
return $defaults;
}
return array_merge($defaults, $this->appends);
}
/**
* getAttribute gets a plain attribute.
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
// Before Event
if (($attr = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {
return $attr;
}
$value = $this->getAttributeFromArray($key);
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// After Event
if (($_attr = $this->fireEvent('model.getAttribute', [$key, $value], true)) !== null) {
return $_attr;
}
return $value;
}
/**
* getAttributeFromArray gets an attribute from the $attributes array.
* @param string $key
* @return mixed
*/
protected function getAttributeFromArray($key)
{
if (array_key_exists($key, $this->attributes)) {
return $this->attributes[$key];
}
}
/**
* hasGetMutator determines if a get mutator exists for an attribute.
* @param string $key
* @return bool
*/
public function hasGetMutator($key)
{
return $this->methodExists('get'.Str::studly($key).'Attribute');
}
/**
* mutateAttribute gets the value of an attribute using its mutator.
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function mutateAttribute($key, $value)
{
return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
/**
* mutateAttributeForArray gets the value of an attribute using its mutator for array conversion.
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function mutateAttributeForArray($key, $value)
{
$value = $this->mutateAttribute($key, $value);
return $value instanceof Arrayable ? $value->toArray() : $value;
}
/**
* setAttribute sets a given attribute on the model.
* @param string $key
* @param mixed $value
* @return $this
*/
public function setAttribute($key, $value)
{
// Before Event
if (($_value = $this->fireEvent('model.beforeSetAttribute', [$key, $value], true)) !== null) {
$value = $_value;
}
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key)) {
$method = 'set'.Str::studly($key).'Attribute';
// If we return the returned value of the mutator call straight away, that will disable the firing of
// 'model.setAttribute' event, and then no third party plugins will be able to implement any kind of
// post processing logic when an attribute is set with explicit mutators. Returning from the mutator
// call will also break method chaining as intended by returning `$this` at the end of this method.
$this->{$method}($value);
}
else {
$this->attributes[$key] = $value;
}
// After Event
$this->fireEvent('model.setAttribute', [$key, $value]);
return $this;
}
/**
* hasSetMutator determines if a set mutator exists for an attribute.
* @param string $key
* @return bool
*/
public function hasSetMutator($key)
{
return $this->methodExists('set'.Str::studly($key).'Attribute');
}
/**
* getAttributes gets all of the current attributes on the model.
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* setRawAttributes sets the array of model attributes. No checking is done.
* @param array $attributes
* @param bool $sync
* @return $this
*/
public function setRawAttributes(array $attributes, $sync = false)
{
$this->attributes = $attributes;
if ($sync) {
$this->syncOriginal();
}
return $this;
}
/**
* getOriginal gets the model's original attribute values.
* @param string|null $key
* @param mixed $default
* @return array
*/
public function getOriginal($key = null, $default = null)
{
return Arr::get($this->original, $key, $default);
}
/**
* syncOriginal attributes with the current.
* @return $this
*/
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
/**
* syncOriginalAttribute syncs a single original attribute with its current value.
* @param string $attribute
* @return $this
*/
public function syncOriginalAttribute($attribute)
{
$this->original[$attribute] = $this->attributes[$attribute];
return $this;
}
/**
* isDirty determines if the model or given attribute(s) have been modified.
* @param array|string|null $attributes
* @return bool
*/
public function isDirty($attributes = null)
{
$dirty = $this->getDirty();
if (is_null($attributes)) {
return count($dirty) > 0;
}
if (!is_array($attributes)) {
$attributes = func_get_args();
}
foreach ($attributes as $attribute) {
if (array_key_exists($attribute, $dirty)) {
return true;
}
}
return false;
}
/**
* getDirty get the attributes that have been changed since last sync.
* @return array
*/
public function getDirty()
{
$dirty = [];
foreach ($this->attributes as $key => $value) {
if (!array_key_exists($key, $this->original)) {
$dirty[$key] = $value;
}
elseif (
$value !== $this->original[$key] &&
!$this->originalIsNumericallyEquivalent($key)
) {
$dirty[$key] = $value;
}
}
foreach ($this->original as $key => $value) {
if (!array_key_exists($key, $this->attributes)) {
$dirty[$key] = null;
}
}
return $dirty;
}
/**
* originalIsNumericallyEquivalent determine if the new and old values for a given key are
* numerically equivalent.
* @param string $key
* @return bool
*/
protected function originalIsNumericallyEquivalent($key)
{
$current = $this->attributes[$key];
$original = $this->original[$key];
return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;
}
/**
* delete the model from the database.
* @return bool|null
* @throws \Exception
*/
public function delete()
{
if (is_null($this->fileName)) {
throw new Exception('No file name (fileName) defined on model.');
}
if ($this->exists) {
if ($this->fireModelEvent('deleting') === false) {
return false;
}
$this->performDeleteOnModel();
$this->exists = false;
// Once the model has been deleted, we will fire off the deleted event so that
// the developers may hook into post-delete operations. We will then return
// a boolean true as the delete is presumably successful on the database.
$this->fireModelEvent('deleted', false);
return true;
}
}
/**
* performDeleteOnModel performs the actual delete query on this model instance.
*/
protected function performDeleteOnModel()
{
$this->newQuery()->delete($this->fileName);
}
/**
* Update the model in the database.
*
* @param array $attributes
* @return bool|int
*/
public function update(array $attributes = [])
{
if (!$this->exists) {
return $this->newQuery()->update($attributes);
}
return $this->fill($attributes)->save();
}
/**
* Save the model to the datasource.
*
* @param array $options
* @return bool
*/
public function save(?array $options = null)
{
return $this->saveInternal(['force' => false] + (array) $options);
}
/**
* Save the model to the database. Is used by {@link save()} and {@link forceSave()}.
* @param array $options
* @return bool
*/
public function saveInternal(array $options = [])
{
// Event
if ($this->fireEvent('model.saveInternal', [$this->attributes, $options], true) === false) {
return false;
}
$query = $this->newQuery();
// If the "saving" event returns false we'll bail out of the save and return
// false, indicating that the save failed. This provides a chance for any
// listeners to cancel save operations if validations fail or whatever.
if ($this->fireModelEvent('saving') === false) {
return false;
}
if ($this->exists) {
$saved = $this->performUpdate($query, $options);
}
else {
$saved = $this->performInsert($query, $options);
}
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
/**
* Finish processing on a successful save operation.
*
* @param array $options
* @return void
*/
protected function finishSave(array $options)
{
$this->fireModelEvent('saved', false);
$this->mtime = $this->newQuery()->lastModified();
$this->syncOriginal();
}
/**
* Perform a model update operation.
*
* @param October\Rain\Halcyon\Builder $query
* @param array $options
* @return bool
*/
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0) {
// If the updating event returns false, we will cancel the update operation so
// developers can hook Validation systems into their models and cancel this
// operation if the model does not pass validation. Otherwise, we update.
if ($this->fireModelEvent('updating') === false) {
return false;
}
// Recheck dirty attributes as they may have change from the updating event
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$query->update($dirty);
$this->fireModelEvent('updated', false);
}
}
return true;
}
/**
* Perform a model insert operation.
*
* @param October\Rain\Halcyon\Builder $query
* @param array $options
* @return bool
*/
protected function performInsert(Builder $query, array $options = [])
{
if ($this->fireModelEvent('creating') === false) {
return false;
}
// Ensure the settings attribute is passed through so this distinction
// is recognized, mainly by the processor.
$attributes = $this->attributesToArray();
$query->insert($attributes);
// We will go ahead and set the exists property to true, so that it is set when
// the created event is fired, just in case the developer tries to update it
// during the event. This will allow them to do so and run an update here.
$this->exists = true;
$this->fireModelEvent('created', false);
return true;
}
/**
* Get a new query builder for the object
* @return \October\Rain\Halcyon\Builder
*/
public function newQuery()
{
$datasource = $this->getDatasource();
$query = new Builder($datasource, $datasource->getPostProcessor());
return $query->setModel($this);
}
/**
* Create a new Halcyon Collection instance.
*
* @param array $models
* @return \October\Rain\Halcyon\Collection
*/
public function newCollection(array $models = [])
{
return new Collection($models);
}
/**
* getFileNameParts returns the base file name and extension.
* Applies a default extension, if none found.
*/
public function getFileNameParts($fileName = null)
{
if ($fileName === null) {
$fileName = $this->fileName;
}
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
if (!strlen($extension)) {
$extension = $this->defaultExtension;
$baseFile = (string) $fileName;
}
else {
$pos = strrpos($fileName, '.');
$baseFile = substr($fileName, 0, $pos);
}
return [$baseFile, $extension];
}
/**
* getDatasource for the model.
*
* @return \October\Rain\Halcyon\Datasource\DatasourceInterface
*/
public function getDatasource()
{
return static::resolveDatasource($this->datasource);
}
/**
* getDatasourceName for the model.
*
* @return string
*/
public function getDatasourceName()
{
return $this->datasource;
}
/**
* setDatasource associated with the model.
*
* @param string $name
* @return $this
*/
public function setDatasource($name)
{
$this->datasource = $name;
return $this;
}
/**
* resolveDatasource instance.
*
* @param string|null $datasource
* @return \October\Rain\Halcyon\Datasource
*/
public static function resolveDatasource($datasource = null)
{
return static::$resolver->datasource($datasource);
}
/**
* getDatasourceResolver instance.
*
* @return \October\Rain\Halcyon\DatasourceResolverInterface
*/
public static function getDatasourceResolver()
{
return static::$resolver;
}
/**
* setDatasourceResolver instance.
*
* @param \October\Rain\Halcyon\Datasource\ResolverInterface $resolver
* @return void
*/
public static function setDatasourceResolver(Resolver $resolver)
{
static::$resolver = $resolver;
}
/**
* unsetDatasourceResolver for models.
*
* @return void
*/
public static function unsetDatasourceResolver()
{
static::$resolver = null;
}
/**
* getCacheManager instance.
*
* @return \Illuminate\Cache\CacheManager
*/
public static function getCacheManager()
{
return static::$cache;
}
/**
* setCacheManager instance.
*
* @param \Illuminate\Cache\CacheManager $cache
* @return void
*/
public static function setCacheManager($cache)
{
static::$cache = $cache;
}
/**
* unsetCacheManager for models.
*
* @return void
*/
public static function unsetCacheManager()
{
static::$cache = null;
}
/**
* initCacheItem initializes the object properties from the cached data. The extra data
* set here becomes available as attributes set on the model after fetch.
* @param array $cached The cached data array.
*/
public static function initCacheItem(&$item)
{
}
/**
* getMutatedAttributes gets the mutated attributes for a given instance.
*
* @return array
*/
public function getMutatedAttributes()
{
$class = static::class;
if (!isset(static::$mutatorCache[$class])) {
static::cacheMutatedAttributes($class);
}
return static::$mutatorCache[$class];
}
/**
* cacheMutatedAttributes extracts and cache all the mutated attributes of a class.
*
* @param string $class
* @return void
*/
public static function cacheMutatedAttributes($class)
{
$mutatedAttributes = [];
// Here we will extract all of the mutated attributes so that we can quickly
// spin through them after we export models to their array form, which we
// need to be fast. This'll let us know the attributes that can mutate.
if (preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches)) {
foreach ($matches[1] as $match) {
$mutatedAttributes[] = lcfirst($match);
}
}
static::$mutatorCache[$class] = $mutatedAttributes;
}
/**
* __get dynamically retrieve attributes on the model.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
if ($this->propertyExists($key)) {
return $this->extendableGet($key);
}
return $this->getAttribute($key);
}
/**
* __set dynamically set attributes on the model.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function __set($key, $value)
{
if ($this->propertyExists($key)) {
$this->extendableSet($key, $value);
}
else {
$this->setAttribute($key, $value);
}
}
/**
* offsetExists determines if the given attribute exists.
*
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset): bool
{
return isset($this->$offset);
}
/**
* offsetGet the value for a given offset.
*
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset): mixed
{
return $this->$offset;
}
/**
* offsetSet the value for a given offset.
*
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value): void
{
$this->$offset = $value;
}
/**
* offsetUnset the value for a given offset.
*
* @param mixed $offset
* @return void
*/
public function offsetUnset($offset): void
{
unset($this->$offset);
}
/**
* __isset determines if an attribute exists on the model.
*
* @param string $key
* @return bool
*/
public function __isset($key)
{
return isset($this->attributes[$key]) ||
(
$this->hasGetMutator($key) &&
!is_null($this->getAttribute($key))
);
}
/**
* __unset an attribute on the model.
*
* @param string $key
* @return void
*/
public function __unset($key)
{
unset($this->attributes[$key]);
}
/**
* __call handles dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
try {
return parent::__call($method, $parameters);
}
catch (BadMethodCallException $ex) {
$query = $this->newQuery();
return call_user_func_array([$query, $method], $parameters);
}
}
/**
* __callStatic handles dynamic static method calls into the method.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array([$instance, $method], $parameters);
}
/**
* __toString converts the model to its string representation.
*
* @return string
*/
public function __toString()
{
return $this->toJson();
}
/**
* __sleep prepare the object for serialization.
*/
public function __sleep()
{
$this->unbindEvent();
$this->extendableDestruct();
return parent::__sleep();
}
/**
* __wakeup when a model is being unserialized, check if it needs to be booted.
*/
public function __wakeup()
{
parent::__wakeup();
$this->bootIfNotBooted();
$this->initializeTraits();
$this->bootNicerEvents();
$this->initializeModelEvent();
}
}
================================================
FILE: src/Halcyon/Processors/Processor.php
================================================
$this->parseTemplateContent($query, $result, $fileName)];
}
/**
* Process the results of a "select" query.
*
* @param \October\Rain\Halcyon\Builder $query
* @param array $results
* @return array
*/
public function processSelect(Builder $query, $results)
{
if (!count($results)) {
return [];
}
$items = [];
foreach ($results as $result) {
$fileName = Arr::get($result, 'fileName');
$items[$fileName] = $this->parseTemplateContent($query, $result, $fileName);
}
return $items;
}
/**
* Helper to break down template content in to a useful array.
* @param int $mtime
* @param string $content
* @return array
*/
protected function parseTemplateContent($query, $result, $fileName)
{
$options = [
'isCompoundObject' => $query->getModel()->isCompoundObject()
];
$content = Arr::get($result, 'content');
$processed = SectionParser::parse($content, $options);
return [
'fileName' => $fileName,
'content' => $content,
'mtime' => Arr::get($result, 'mtime'),
'markup' => $processed['markup'],
'code' => $processed['code']
] + $processed['settings'];
}
/**
* Process the data in to an insert action.
*
* @param \October\Rain\Halcyon\Builder $query
* @param array $data
* @return string
*/
public function processInsert(Builder $query, $data)
{
$options = [
'wrapCodeInPhpTags' => $query->getModel()->getWrapCode(),
'isCompoundObject' => $query->getModel()->isCompoundObject()
];
return SectionParser::render($data, $options);
}
/**
* Process the data in to an update action.
*
* @param \October\Rain\Halcyon\Builder $query
* @param array $data
* @return string
*/
public function processUpdate(Builder $query, $data)
{
$options = [
'wrapCodeInPhpTags' => $query->getModel()->getWrapCode(),
'isCompoundObject' => $query->getModel()->isCompoundObject()
];
$existingData = $query->getModel()->attributesToArray();
return SectionParser::render($data + $existingData, $options);
}
}
================================================
FILE: src/Halcyon/Processors/SectionParser.php
================================================
true,
'isCompoundObject' => true
], $options));
if (!$isCompoundObject) {
return $data['content'] ?? '';
}
$iniParser = new Ini;
$code = trim($data['code'] ?? '');
$markup = trim($data['markup'] ?? '');
$trim = function (&$values) use (&$trim) {
foreach ($values as &$value) {
if (!is_array($value)) {
$value = trim($value);
}
else {
$trim($value);
}
}
};
$settings = $data['settings'] ?? [];
$trim($settings);
// Build content
$content = [];
// Settings section
if ($settings) {
$content[] = self::cleanTemplateSection($iniParser->render($settings));
}
// Code section
if ($code) {
if ($wrapCodeInPhpTags) {
$code = preg_replace('/^\<\?php/', '', $code);
$code = preg_replace('/^\<\?/', '', $code);
$code = preg_replace('/\?>$/', '', $code);
$code = trim($code, PHP_EOL);
$content[] = '';
}
else {
$content[] = $code;
}
}
// Content section
$content[] = self::cleanTemplateSection($markup);
// Assemble template content
$content = trim(implode(PHP_EOL.self::SECTION_SEPARATOR.PHP_EOL, $content));
return $content;
}
/**
* parse a CMS object file content.
* The expected file format is following:
*
* If the content has only 2 sections they are considered as settings and Twig.
* If there is only a single section, it is considered as Twig.
*
* Returns an array with the following indexes: 'settings', 'markup', 'code'.
* The 'markup' and 'code' elements contain strings. The 'settings' element contains the
* parsed INI file as array. If the content string doesn't contain a section, the corresponding
* result element has null value.
* @param string $content
* @return array
*/
public static function parse($content, $options = [])
{
extract(array_merge([
'isCompoundObject' => true
], $options));
$result = [
'settings' => [],
'code' => null,
'markup' => null
];
if (!$isCompoundObject || !strlen((string) $content)) {
return $result;
}
$iniParser = new Ini;
$sections = self::splitContentSections($content);
$count = count($sections);
foreach ($sections as &$section) {
$section = trim($section);
}
if ($count >= 3) {
$result['settings'] = @$iniParser->parse($sections[0], true)
?: [self::ERROR_INI => $sections[0]];
$result['code'] = $sections[1];
$result['code'] = preg_replace('/^\s*\<\?php/', '', $result['code']);
$result['code'] = preg_replace('/^\s*\<\?/', '', $result['code']);
$result['code'] = preg_replace('/\?\>\s*$/', '', $result['code']);
$result['code'] = trim($result['code'], PHP_EOL);
$result['markup'] = $sections[2];
}
elseif ($count === 2) {
$result['settings'] = @$iniParser->parse($sections[0], true)
?: [self::ERROR_INI => $sections[0]];
$result['markup'] = $sections[1];
}
elseif ($count === 1) {
$result['markup'] = $sections[0];
}
return $result;
}
/**
* parseOffset is the same as parse method, except using the line number where the
* respective section begins is returned. Returns an array with the following indexes:
* 'settings', 'markup', 'code'.
* @param string $content
* @return array
*/
public static function parseOffset($content)
{
$content = Str::normalizeEol($content);
$sections = self::splitContentSections($content);
$count = count($sections);
$result = [
'settings' => null,
'code' => null,
'markup' => null
];
if ($count >= 3) {
$result['settings'] = self::adjustLinePosition($content);
$result['code'] = self::calculateLinePosition($content);
$result['markup'] = self::calculateLinePosition($content, 2);
}
elseif ($count === 2) {
$result['settings'] = self::adjustLinePosition($content);
$result['markup'] = self::calculateLinePosition($content);
}
elseif ($count === 1) {
$result['markup'] = 1;
}
return $result;
}
/**
* cleanTemplateSection ensures the content does not attempt to escape its section
* by using the separator sequence. The content separator is simply removed.
*/
protected static function cleanTemplateSection($content)
{
return implode('', self::splitContentSections($content));
}
/**
* splitContentSections splits a block of content in to sections,
* split by the section separator (==).
* @param string $content
* @return array
*/
protected static function splitContentSections($content)
{
return preg_split('/^'.preg_quote(self::SECTION_SEPARATOR).'\s*$/m', $content, -1);
}
/**
* calculateLinePosition returns the line number of a found instance of CMS object
* section separator (==). Returns the line number the instance was found.
* @param string $content
* @param int $instance
* @return int
*/
protected static function calculateLinePosition($content, $instance = 1)
{
$count = 0;
$lines = explode(PHP_EOL, $content);
foreach ($lines as $number => $line) {
if (trim($line) === self::SECTION_SEPARATOR) {
$count++;
}
if ($count === $instance) {
return static::adjustLinePosition($content, $number);
}
}
return null;
}
/**
* adjustLinePosition pushes the starting line number forward since it is not always directly
* after the separator (==). There can be an opening tag or white space in between
* where the section really begins. The startLine is the calculated starting line
* from calculateLinePosition(). Returns the adjusted line number.
* @param string $content
* @param int $startLine
* @return int
*/
protected static function adjustLinePosition($content, $startLine = -1)
{
// Account for the separator itself
$startLine++;
$lines = array_slice(explode(PHP_EOL, $content), $startLine);
foreach ($lines as $line) {
$line = trim($line);
// Empty line
if ($line === '') {
$startLine++;
continue;
}
// PHP line
if ($line === ' $datasource]);
$resolver->setDefaultDatasource('theme1');
Model::setDatasourceResolver($resolver);
```
### Model example
Inherit the `October\Rain\Halcyon\Model` to create a new model:
```php
'my-file',
'title' => 'Test page',
'markup' => '
Hello world!
'
]);
```
Executing the above code will create a new file **/path/to/theme/pages/my-file.htm**, with the following contents:
```twig
title = "Test page"
==
Hello world!
```
We can find the page and use it later:
```php
$page = MyPage::find('my-file');
echo '
'.$page->title.'
';
echo $page->markup;
```
If we change the file name, it will be renamed on the file system too:
```php
// New file path: /path/to/theme/pages/renamed-file.htm
$page->fileName = 'renamed-file';
$page->save();
```
================================================
FILE: src/Halcyon/Traits/Validation.php
================================================
bindEvent('model.saveInternal', function ($data, $options) use ($model) {
// If forcing the save event, the beforeValidate/afterValidate
// events should still fire for consistency. So validate an
// empty set of rules and messages.
$force = Arr::get($options, 'force', false);
if ($force) {
$valid = $model->validate([], []);
}
else {
$valid = $model->validate();
}
if (!$valid) {
return false;
}
}, 500);
});
}
/**
* getValidationAttributes returns the model data used for validation.
* @return array
*/
protected function getValidationAttributes()
{
return $this->getAttributes();
}
/**
* makeValidator instantiates the validator used by the validation process, depending if the class is being used inside or
* outside of Laravel.
* @return \Illuminate\Validation\Validator
*/
protected static function makeValidator($data, $rules, $customMessages, $attributeNames)
{
return static::getModelValidator()->make($data, $rules, $customMessages, $attributeNames);
}
/**
* forceSave the model even if validation fails.
* @return bool
*/
public function forceSave($options = null)
{
return $this->saveInternal(['force' => true] + (array) $options);
}
/**
* validate the model instance
* @return bool
*/
public function validate($rules = null, $customMessages = null, $attributeNames = null)
{
if ($this->validationErrors === null) {
$this->validationErrors = new MessageBag;
}
$throwOnValidation = property_exists($this, 'throwOnValidation')
? $this->throwOnValidation
: true;
if (($this->fireModelEvent('validating') === false) || ($this->fireEvent('model.beforeValidate') === false)) {
if ($throwOnValidation) {
throw new ModelException($this);
}
return false;
}
if ($this->methodExists('beforeValidate')) {
$this->beforeValidate();
}
// Perform validation
$rules = is_null($rules) ? $this->rules : $rules;
$rules = $this->processValidationRules($rules);
$success = true;
if (!empty($rules)) {
$data = $this->getValidationAttributes();
$lang = static::getModelValidator()->getTranslator();
// Custom messages, translate internal references
if (property_exists($this, 'customMessages') && is_null($customMessages)) {
$customMessages = $this->customMessages;
}
if (is_null($customMessages)) {
$customMessages = [];
}
$translatedCustomMessages = [];
foreach ($customMessages as $rule => $customMessage) {
$translatedCustomMessages[$rule] = $lang->get($customMessage);
}
$customMessages = $translatedCustomMessages;
// Attribute names, translate internal references
if (is_null($attributeNames)) {
$attributeNames = [];
}
if (property_exists($this, 'attributeNames')) {
$attributeNames = array_merge($this->attributeNames, $attributeNames);
}
$translatedAttributeNames = [];
foreach ($attributeNames as $attribute => $attributeName) {
$translatedAttributeNames[$attribute] = $lang->get($attributeName);
}
$attributeNames = $translatedAttributeNames;
// Translate any externally defined attribute names
$translations = $lang->get('validation.attributes');
if (is_array($translations)) {
$attributeNames = array_merge($translations, $attributeNames);
}
// Hand over to the validator
$validator = static::makeValidator($data, $rules, $customMessages, $attributeNames);
$success = $validator->passes();
if ($success) {
if ($this->validationErrors->count() > 0) {
$this->validationErrors = new MessageBag;
}
}
else {
$this->validationErrors = $validator->messages();
// Flash input, if available
if (
($input = Input::getFacadeRoot()) &&
method_exists($input, 'hasSession') &&
$input->hasSession()
) {
$input->flash();
}
}
}
$this->fireModelEvent('validated', false);
$this->fireEvent('model.afterValidate');
if ($this->methodExists('afterValidate')) {
$this->afterValidate();
}
if (!$success && $throwOnValidation) {
throw new ModelException($this);
}
return $success;
}
/**
* processValidationRules
*/
protected function processValidationRules($rules)
{
// Run through field names and convert array notation field names to dot notation
$rules = $this->processRuleFieldNames($rules);
foreach ($rules as $field => $ruleParts) {
// Trim empty rules
if (is_string($ruleParts) && trim($ruleParts) === '') {
unset($rules[$field]);
continue;
}
// Normalize rulesets
if (!is_array($ruleParts)) {
$ruleParts = explode('|', $ruleParts);
}
// Analyse each rule individually
foreach ($ruleParts as $key => $rulePart) {
// Look for required:create and required:update rules
if (str_starts_with($rulePart, 'required:create') && $this->exists) {
unset($ruleParts[$key]);
}
elseif (str_starts_with($rulePart, 'required:update') && !$this->exists) {
unset($ruleParts[$key]);
}
}
$rules[$field] = $ruleParts;
}
return $rules;
}
/**
* processRuleFieldNames converts any field names using array notation
* (ie. `field[child]`) into dot notation (ie. `field.child`)
* @param array $rules
* @return array
*/
protected function processRuleFieldNames($rules)
{
$processed = [];
foreach ($rules as $field => $ruleParts) {
$fieldName = $field;
if (preg_match('/^.*?\[.*?\]/', $fieldName)) {
$fieldName = str_replace('[]', '.*', $fieldName);
$fieldName = str_replace(['[', ']'], ['.', ''], $fieldName);
}
$processed[$fieldName] = $ruleParts;
}
return $processed;
}
/**
* isAttributeRequired determines if an attribute is required based on the validation rules.
* @param string $attribute
* @return bool
*/
public function isAttributeRequired($attribute)
{
if (!isset($this->rules[$attribute])) {
return false;
}
$ruleset = $this->rules[$attribute];
if (is_array($ruleset)) {
$ruleset = implode('|', $ruleset);
}
if (strpos($ruleset, 'required:create') !== false && $this->exists) {
return false;
}
if (strpos($ruleset, 'required:update') !== false && !$this->exists) {
return false;
}
if (strpos($ruleset, 'required_with') !== false) {
$requiredWith = substr($ruleset, strpos($ruleset, 'required_with') + 14);
$requiredWith = substr($requiredWith, 0, strpos($requiredWith, '|'));
return $this->isAttributeRequired($requiredWith);
}
return strpos($ruleset, 'required') !== false;
}
/**
* errors gets validation error message collection for the Model
* @return \Illuminate\Support\MessageBag
*/
public function errors()
{
return $this->validationErrors;
}
/**
* validating creates a new native event for handling beforeValidate().
* @param Closure|string $callback
* @return void
*/
public static function validating($callback)
{
static::registerModelEvent('validating', $callback);
}
/**
* validated creates a new native event for handling afterValidate().
* @param Closure|string $callback
* @return void
*/
public static function validated($callback)
{
static::registerModelEvent('validated', $callback);
}
/**
* getModelValidator instance.
* @return \Illuminate\Validation\Validator
*/
public static function getModelValidator()
{
if (static::$validator === null) {
static::$validator = Validator::getFacadeRoot();
}
return static::$validator;
}
/**
* setModelValidator instance.
* @param \Illuminate\Validation\Validator
* @return void
*/
public static function setModelValidator($validator)
{
static::$validator = $validator;
}
/**
* unsetModelValidator for models.
* @return void
*/
public static function unsetModelValidator()
{
static::$validator = null;
}
}
================================================
FILE: src/Html/BlockBuilder.php
================================================
startBlock($name);
}
/**
* startBlock begins the layout block
*/
public function startBlock(string $name)
{
array_push($this->blockStack, $name);
ob_start();
}
/**
* endPut is a helper for endBlock and also clears the output buffer
* Append indicates that the new content should be appended to the existing block content
*/
public function endPut(bool $append = false)
{
$this->endBlock($append);
}
/**
* endBlock closes the layout block
* Append indicates that the new content should be appended to the existing block content
*/
public function endBlock(bool $append = false)
{
if (!count($this->blockStack)) {
throw new Exception('Invalid block nesting');
}
$name = array_pop($this->blockStack);
$contents = ob_get_clean();
if ($append) {
$this->append($name, $contents);
}
else {
$this->blocks[$name] = $contents;
}
}
/**
* set a content of the layout block.
*/
public function set(string $name, $content)
{
$this->blocks[$name] = $content;
}
/**
* append a content of the layout block
*/
public function append(string $name, $content)
{
if (!isset($this->blocks[$name])) {
$this->blocks[$name] = '';
}
$this->blocks[$name] .= $content;
}
/**
* placeholder returns the layout block contents and deletes the block from memory.
*/
public function placeholder(string $name, $default = null)
{
$result = $this->get($name, $default);
unset($this->blocks[$name]);
if (is_string($result)) {
$result = trim($result);
}
return $result;
}
/**
* has a placeholder set up
*/
public function has(string $name): bool
{
return isset($this->blocks[$name]);
}
/**
* get returns the layout block contents but not deletes the block from memory
*/
public function get(string $name, $default = null)
{
if (!isset($this->blocks[$name])) {
return $default;
}
return $this->blocks[$name];
}
/**
* reset clears all the registered blocks
*/
public function reset()
{
$this->blockStack = [];
$this->blocks = [];
}
}
================================================
FILE: src/Html/FormBuilder.php
================================================
url = $url;
$this->html = $html;
$this->csrfToken = $csrfToken;
$this->sessionKey = $sessionKey;
}
/**
* open up a new HTML form and includes a session key.
* @param array $options
* @return string
*/
public function open(array $options = [])
{
$method = strtoupper(Arr::get($options, 'method', 'post'));
$request = Arr::get($options, 'request');
$model = Arr::get($options, 'model');
if ($model) {
$this->model = $model;
}
$append = $this->requestHandler($request);
if ($method !== 'GET') {
$append .= $this->sessionKey(Arr::get($options, 'sessionKey'));
}
$attributes = [];
// We need to extract the proper method from the attributes. If the method is
// something other than GET or POST we'll use POST since we will spoof the
// actual method since forms don't support the reserved methods in HTML.
$attributes['method'] = $this->getMethod($method);
$attributes['action'] = $this->getAction($options);
$attributes['accept-charset'] = 'UTF-8';
// If the method is PUT, PATCH or DELETE we will need to add a spoofer hidden
// field that will instruct the Symfony request to pretend the method is a
// different method than it actually is, for convenience from the forms.
$append .= $this->getAppendage($method);
if (isset($options['files']) && $options['files']) {
$options['enctype'] = 'multipart/form-data';
}
// Finally we're ready to create the final form HTML field. We will attribute
// format the array of attributes. We will also add on the appendage which
// is used to spoof requests for this PUT, PATCH, etc. methods on forms.
$attributes = array_merge(
$attributes,
Arr::except($options, $this->reserved)
);
// Finally, we will concatenate all of the attributes into a single string so
// we can build out the final form open statement. We'll also append on an
// extra value for the hidden _method field if it's needed for the form.
$attributes = $this->html->attributes($attributes);
return '';
}
/**
* token generates a hidden field with the current CSRF token.
* @return string
*/
public function token()
{
$token = !empty($this->csrfToken)
? $this->csrfToken
: $this->session->token();
return $this->hidden('_token', $token);
}
/**
* label creates a form label element.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function label($name, $value = null, $options = [])
{
$this->labels[] = $name;
$options = $this->html->attributes($options);
$value = e($this->formatLabel($name, $value));
return '';
}
/**
* formatLabel value.
* @param string $name
* @param string|null $value
* @return string
*/
protected function formatLabel($name, $value)
{
return $value ?: ucwords(str_replace('_', ' ', $name));
}
/**
* input creates a form input field.
* @param string $type
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function input($type, $name, $value = null, $options = [])
{
if (!isset($options['name'])) {
$options['name'] = $name;
}
// We will get the appropriate value for the given field. We will look for the
// value in the session for the value in the old input data then we'll look
// in the model instance if one is set. Otherwise we will just use empty.
$id = $this->getIdAttribute($name, $options);
if (!in_array($type, $this->skipValueTypes)) {
$value = $this->getValueAttribute($name, $value);
}
// Once we have the type, value, and ID we can merge them into the rest of the
// attributes array so we can convert them into their HTML attribute format
// when creating the HTML element. Then, we will return the entire input.
$merge = compact('type', 'value', 'id');
$options = array_merge($options, $merge);
return 'html->attributes($options).'>';
}
/**
* text input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function text($name, $value = null, $options = [])
{
return $this->input('text', $name, $value, $options);
}
/**
* password input field.
* @param string $name
* @param array $options
* @return string
*/
public function password($name, $options = [])
{
return $this->input('password', $name, '', $options);
}
/**
* hidden input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function hidden($name, $value = null, $options = [])
{
return $this->input('hidden', $name, $value, $options);
}
/**
* email input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function email($name, $value = null, $options = [])
{
return $this->input('email', $name, $value, $options);
}
/**
* number input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function number($name, $value = null, $options = [])
{
return $this->input('number', $name, $value, $options);
}
/**
* url input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function url($name, $value = null, $options = [])
{
return $this->input('url', $name, $value, $options);
}
/**
* file input field.
* @param string $name
* @param array $options
* @return string
*/
public function file($name, $options = [])
{
return $this->input('file', $name, null, $options);
}
//
// Textarea
//
/**
* textarea input field.
* @param string $name
* @param string $value
* @param array $options
* @return string
*/
public function textarea($name, $value = null, $options = [])
{
if (!isset($options['name'])) {
$options['name'] = $name;
}
// Next we will look for the rows and cols attributes, as each of these are put
// on the textarea element definition. If they are not present, we will just
// assume some sane default values for these attributes for the developer.
$options = $this->setTextAreaSize($options);
$options['id'] = $this->getIdAttribute($name, $options);
$value = (string) $this->getValueAttribute($name, $value);
unset($options['size']);
// Next we will convert the attributes into a string form. Also we have removed
// the size attribute, as it was merely a short-cut for the rows and cols on
// the element. Then we'll create the final textarea elements HTML for us.
$options = $this->html->attributes($options);
return '';
}
/**
* setTextAreaSize on the attributes.
* @param array $options
* @return array
*/
protected function setTextAreaSize($options)
{
if (isset($options['size'])) {
return $this->setQuickTextAreaSize($options);
}
// If the "size" attribute was not specified, we will just look for the regular
// columns and rows attributes, using sane defaults if these do not exist on
// the attributes array. We'll then return this entire options array back.
$cols = Arr::get($options, 'cols', 50);
$rows = Arr::get($options, 'rows', 10);
return array_merge($options, compact('cols', 'rows'));
}
/**
* setQuickTextAreaSize using the quick "size" attribute.
*
* @param array $options
* @return array
*/
protected function setQuickTextAreaSize($options)
{
$segments = explode('x', $options['size']);
return array_merge($options, ['cols' => $segments[0], 'rows' => $segments[1]]);
}
//
// Select
//
/**
* select box field with empty option support.
* @param string $name
* @param array $list
* @param string $selected
* @param array $options
* @return string
*/
public function select($name, $list = [], $selected = null, $options = [])
{
if (array_key_exists('emptyOption', $options)) {
$list = ['' => $options['emptyOption']] + $list;
}
$selectOptions = false;
if (array_key_exists('selectOptions', $options)) {
$selectOptions = $options['selectOptions'] === true;
unset($options['selectOptions']);
}
// When building a select box the "value" attribute is really the selected one
// so we will use that when checking the model or session for a value which
// should provide a convenient method of re-populating the forms on post.
$selected = $this->getValueAttribute($name, $selected);
$options['id'] = $this->getIdAttribute($name, $options);
if (!isset($options['name'])) {
$options['name'] = $name;
}
// We will simply loop through the options and build an HTML value for each of
// them until we have an array of HTML declarations. Then we will join them
// all together into one single HTML element that can be put on the form.
$html = [];
foreach ($list as $value => $display) {
$html[] = $this->getSelectOption($display, $value, $selected);
}
// Once we have all of this HTML, we can join this into a single element after
// formatting the attributes into an HTML "attributes" string, then we will
// build out a final select statement, which will contain all the values.
$options = $this->html->attributes($options);
$list = implode('', $html);
return $selectOptions ? $list : "";
}
/**
* selectOptions only renders the options inside a select.
* @param string $name
* @param array $list
* @param string $selected
* @param array $options
* @return string
*/
public function selectOptions($name, $list = [], $selected = null, $options = [])
{
return $this->select($name, $list, $selected, ['selectOptions' => true] + $options);
}
/**
* selectRange field.
* @param string $name
* @param string $begin
* @param string $end
* @param string $selected
* @param array $options
* @return string
*/
public function selectRange($name, $begin, $end, $selected = null, $options = [])
{
$range = array_combine($range = range($begin, $end), $range);
return $this->select($name, $range, $selected, $options);
}
/**
* selectYear field.
* @param string $name
* @param string $begin
* @param string $end
* @param string $selected
* @param array $options
* @return string
*/
public function selectYear()
{
return call_user_func_array([$this, 'selectRange'], func_get_args());
}
/**
* selectMonth field.
* @param string $name
* @param string $selected
* @param array $options
* @param string $format DateTime format string (default: 'F' for full month name)
* @return string
*/
public function selectMonth($name, $selected = null, $options = [], $format = 'F')
{
$months = [];
foreach (range(1, 12) as $month) {
$date = new \DateTime("2024-{$month}-01");
$months[$month] = $date->format($format);
}
return $this->select($name, $months, $selected, $options);
}
/**
* getSelectOption for the given value.
* @param string $display
* @param string $value
* @param string $selected
* @return string
*/
public function getSelectOption($display, $value, $selected)
{
if (is_array($display)) {
return $this->optionGroup($display, $value, $selected);
}
return $this->option($display, $value, $selected);
}
/**
* optionGroup form element.
* @param array $list
* @param string $label
* @param string $selected
* @return string
*/
protected function optionGroup($list, $label, $selected)
{
$html = [];
foreach ($list as $value => $display) {
$html[] = $this->option($display, $value, $selected);
}
return '';
}
/**
* option for a select element option.
* @param string $display
* @param string $value
* @param string $selected
* @return string
*/
protected function option($display, $value, $selected)
{
$selected = $this->getSelectedValue($value, $selected);
$options = ['value' => e($value), 'selected' => $selected];
return '';
}
/**
* getSelectedValue determines if the value is selected.
* @param string $value
* @param string $selected
* @return string
*/
protected function getSelectedValue($value, $selected)
{
if (is_array($selected)) {
return in_array($value, $selected) ? 'selected' : null;
}
return ((string) $value === (string) $selected) ? 'selected' : null;
}
//
// Checkbox
//
/**
* checkbox input field.
* @param string $name
* @param mixed $value
* @param bool $checked
* @param array $options
* @return string
*/
public function checkbox($name, $value = 1, $checked = null, $options = [])
{
return $this->checkable('checkbox', $name, $value, $checked, $options);
}
/**
* radio button input field.
* @param string $name
* @param mixed $value
* @param bool $checked
* @param array $options
* @return string
*/
public function radio($name, $value = null, $checked = null, $options = [])
{
if (is_null($value)) {
$value = $name;
}
return $this->checkable('radio', $name, $value, $checked, $options);
}
/**
* checkable input field.
* @param string $type
* @param string $name
* @param mixed $value
* @param bool $checked
* @param array $options
* @return string
*/
protected function checkable($type, $name, $value, $checked, $options)
{
$checked = $this->getCheckedState($type, $name, $value, $checked);
if ($checked) {
$options['checked'] = 'checked';
}
return $this->input($type, $name, $value, $options);
}
/**
* getCheckedState for a checkable input.
* @param string $type
* @param string $name
* @param mixed $value
* @param bool $checked
* @return bool
*/
protected function getCheckedState($type, $name, $value, $checked)
{
switch ($type) {
case 'checkbox':
return $this->getCheckboxCheckedState($name, $value, $checked);
case 'radio':
return $this->getRadioCheckedState($name, $value, $checked);
default:
return $this->getValueAttribute($name) === $value;
}
}
/**
* getCheckboxCheckedState for a checkbox input.
* @param string $name
* @param mixed $value
* @param bool $checked
* @return bool
*/
protected function getCheckboxCheckedState($name, $value, $checked)
{
if (
isset($this->session) &&
!$this->oldInputIsEmpty() &&
is_null($this->old($name))
) {
return false;
}
if ($this->missingOldAndModel($name)) {
return $checked;
}
$posted = $this->getValueAttribute($name);
return is_array($posted) ? in_array($value, $posted) : (bool) $posted;
}
/**
* getRadioCheckedState for a radio input.
* @param string $name
* @param mixed $value
* @param bool $checked
* @return bool
*/
protected function getRadioCheckedState($name, $value, $checked)
{
if ($this->missingOldAndModel($name)) {
return $checked;
}
return $this->getValueAttribute($name) === $value;
}
/**
* missingOldAndModel determines if old input or model input exists for a key.
* @param string $name
* @return bool
*/
protected function missingOldAndModel($name)
{
return (is_null($this->old($name)) && is_null($this->getModelValueAttribute($name)));
}
/**
* reset input element.
* @param string $value
* @param array $attributes
* @return string
*/
public function reset($value, $attributes = [])
{
return $this->input('reset', null, $value, $attributes);
}
/**
* image input element.
* @param string $url
* @param string $name
* @param array $attributes
* @return string
*/
public function image($url, $name = null, $attributes = [])
{
$attributes['src'] = $this->url->asset($url);
return $this->input('image', $name, null, $attributes);
}
/**
* submit button element.
* @param string $value
* @param array $options
* @return string
*/
public function submit($value = null, $options = [])
{
return $this->input('submit', null, $value, $options);
}
/**
* button element.
* @param string $value
* @param array $options
* @return string
*/
public function button($value = null, $options = [])
{
if (!array_key_exists('type', $options)) {
$options['type'] = 'button';
}
return '';
}
/**
* getMethod parses the form action method.
* @param string $method
* @return string
*/
protected function getMethod($method)
{
$method = strtoupper($method);
return $method !== 'GET' ? 'POST' : $method;
}
/**
* getAction gets the form action from the options.
* @param array $options
* @return string
*/
protected function getAction(array $options)
{
// We will also check for a "route" or "action" parameter on the array so that
// developers can easily specify a route or controller action when creating
// a form providing a convenient interface for creating the form actions.
if (isset($options['url'])) {
return $this->getUrlAction($options['url']);
}
if (isset($options['route'])) {
return $this->getRouteAction($options['route']);
}
// If an action is available, we are attempting to open a form to a controller
// action route. So, we will use the URL generator to get the path to these
// actions and return them from the method. Otherwise, we'll use current.
elseif (isset($options['action'])) {
return $this->getControllerAction($options['action']);
}
return $this->url->current();
}
/**
* getUrlAction gets the action for a "url" option.
* @param array|string $options
* @return string
*/
protected function getUrlAction($options)
{
if (is_array($options)) {
return $this->url->to($options[0], array_slice($options, 1));
}
return $this->url->to($options);
}
/**
* getRouteAction gets the action for a "route" option.
* @param array|string $options
* @return string
*/
protected function getRouteAction($options)
{
if (is_array($options)) {
return $this->url->route($options[0], array_slice($options, 1));
}
return $this->url->route($options);
}
/**
* getControllerAction gets the action for an "action" option.
* @param array|string $options
* @return string
*/
protected function getControllerAction($options)
{
if (is_array($options)) {
return $this->url->action($options[0], array_slice($options, 1));
}
return $this->url->action($options);
}
/**
* getAppendage gets the form appendage for the given method.
* @param string $method
* @return string
*/
protected function getAppendage($method)
{
list($method, $appendage) = [strtoupper($method), ''];
// If the HTTP method is in this list of spoofed methods, we will attach the
// method spoofer hidden input to the form. This allows us to use regular
// form to initiate PUT and DELETE requests in addition to the typical.
if (in_array($method, $this->spoofedMethods)) {
$appendage .= $this->hidden('_method', $method);
}
// If the method is something other than GET we will go ahead and attach the
// CSRF token to the form, as this can't hurt and is convenient to simply
// always have available on every form the developers creates for them.
if ($method !== 'GET') {
$appendage .= $this->token();
}
return $appendage;
}
/**
* getIdAttribute for a field name.
* @param string $name
* @param array $attributes
* @return string
*/
public function getIdAttribute($name, $attributes)
{
if (array_key_exists('id', $attributes)) {
return $attributes['id'];
}
if (in_array($name, $this->labels)) {
return $name;
}
}
/**
* getValueAttribute that should be assigned to the field.
* @param string $name
* @param string $value
* @return string
*/
public function getValueAttribute($name, $value = null)
{
if (is_null($name)) {
return $value;
}
if (!is_null($this->old($name))) {
return $this->old($name);
}
if (!is_null($value)) {
return $value;
}
if (isset($this->model)) {
return $this->getModelValueAttribute($name);
}
}
/**
* getModelValueAttribute that should be assigned to the field.
* @param string $name
* @return string
*/
protected function getModelValueAttribute($name)
{
if (is_object($this->model)) {
return object_get($this->model, $this->transformKey($name));
}
elseif (is_array($this->model)) {
return Arr::get($this->model, $this->transformKey($name));
}
}
/**
* old gets a value from the session's old input.
* @param string $name
* @return string
*/
public function old($name)
{
if (isset($this->session)) {
return $this->session->getOldInput($this->transformKey($name));
}
}
/**
* oldInputIsEmpty determines if the old input is empty.
* @return bool
*/
public function oldInputIsEmpty()
{
return (isset($this->session) && count($this->session->getOldInput()) === 0);
}
/**
* transformKey from array to dot syntax.
* @param string $key
* @return string
*/
protected function transformKey($key)
{
return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $key);
}
/**
* getSessionStore implementation.
* @return \Illuminate\Session\Store $session
*/
public function getSessionStore()
{
return $this->session;
}
/**
* setSessionStore implementation.
* @param \Illuminate\Session\Store $session
* @return $this
*/
public function setSessionStore(Session $session)
{
$this->session = $session;
return $this;
}
/**
* value is a helper for getting form values. Tries to find the old value,
* then uses a postback/get value, then looks at the form model values.
* @param string $name
* @param string $value
* @return string
*/
public function value($name, $value = null)
{
if (is_null($name)) {
return $value;
}
if (!is_null($this->old($name))) {
return $this->old($name);
}
$inputValue = Request::input(Helper::nameToDot($name));
if (!is_null($inputValue)) {
return $inputValue;
}
if (isset($this->model)) {
return $this->getModelValueAttribute($name);
}
return $value;
}
/**
* requestHandler returns a hidden HTML input, supplying the postback request handler.
* @return string
*/
protected function requestHandler($name = null)
{
if (!$name) {
return '';
}
return $this->hidden('_handler', $name);
}
/**
* sessionKey returns a hidden HTML input, supplying the session key value.
* @return string
*/
public function sessionKey($sessionKey = null)
{
if (!$sessionKey) {
$sessionKey = Request::post('_session_key', $this->sessionKey);
}
return $this->hidden('_session_key', $sessionKey);
}
/**
* getSessionKey returns the active session key, used fr deferred bindings.
* @return string
*/
public function getSessionKey()
{
return $this->sessionKey;
}
}
================================================
FILE: src/Html/Helper.php
================================================
url = $url;
}
/**
* entities converts an HTML string to entities.
*
* @param string $value
* @return string
*/
public function entities($value)
{
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}
/**
* decode converts entities to HTML characters.
*
* @param string $value
* @return string
*/
public function decode($value)
{
return html_entity_decode($value, ENT_QUOTES, 'UTF-8');
}
/**
* script generates a link to a JavaScript file.
*
* @param string $url
* @param array $attributes
* @param bool $secure
* @return string
*/
public function script($url, $attributes = [], $secure = null)
{
$attributes['src'] = $this->url->asset($url, $secure);
return ''.PHP_EOL;
}
/**
* style generates a link to a CSS file.
*
* @param string $url
* @param array $attributes
* @param bool $secure
* @return string
*/
public function style($url, $attributes = [], $secure = null)
{
$defaults = ['media' => 'all', 'type' => 'text/css', 'rel' => 'stylesheet'];
$attributes = $attributes + $defaults;
$attributes['href'] = $this->url->asset($url, $secure);
return 'attributes($attributes).'>'.PHP_EOL;
}
/**
* image generates an HTML image element.
*
* @param string $url
* @param string $alt
* @param array $attributes
* @param bool $secure
* @return string
*/
public function image($url, $alt = null, $attributes = [], $secure = null)
{
$attributes['alt'] = $alt;
return 'attributes($attributes).'>';
}
/**
* link generates a HTML link.
*
* @param string $url
* @param string $title
* @param array $attributes
* @param bool $secure
* @return string
*/
public function link($url, $title = null, $attributes = [], $secure = null)
{
$url = $this->url->to($url, [], $secure);
if (is_null($title) || $title === false) {
$title = $url;
}
return 'attributes($attributes).'>'.$this->entities($title).'';
}
/**
* secureLink generates a HTTPS HTML link.
*
* @param string $url
* @param string $title
* @param array $attributes
* @return string
*/
public function secureLink($url, $title = null, $attributes = [])
{
return $this->link($url, $title, $attributes, true);
}
/**
* linkAsset generates a HTML link to an asset.
*
* @param string $url
* @param string $title
* @param array $attributes
* @param bool $secure
* @return string
*/
public function linkAsset($url, $title = null, $attributes = [], $secure = null)
{
$url = $this->url->asset($url, $secure);
return $this->link($url, $title ?: $url, $attributes, $secure);
}
/**
* linkSecureAsset generates a HTTPS HTML link to an asset.
*
* @param string $url
* @param string $title
* @param array $attributes
* @return string
*/
public function linkSecureAsset($url, $title = null, $attributes = [])
{
return $this->linkAsset($url, $title, $attributes, true);
}
/**
* linkRoute generates a HTML link to a named route.
*
* @param string $name
* @param string $title
* @param array $parameters
* @param array $attributes
* @return string
*/
public function linkRoute($name, $title = null, $parameters = [], $attributes = [])
{
return $this->link($this->url->route($name, $parameters), $title, $attributes);
}
/**
* linkAction generates a HTML link to a controller action.
*
* @param string $action
* @param string $title
* @param array $parameters
* @param array $attributes
* @return string
*/
public function linkAction($action, $title = null, $parameters = [], $attributes = [])
{
return $this->link($this->url->action($action, $parameters), $title, $attributes);
}
/**
* mailto generates a HTML link to an email address.
*
* @param string $email
* @param string $title
* @param array $attributes
* @return string
*/
public function mailto($email, $title = null, $attributes = [])
{
$email = $this->email($email);
$title = $title ?: $email;
$email = $this->obfuscate('mailto:') . $email;
return 'attributes($attributes).'>'.$this->entities($title).'';
}
/**
* email obfuscates an e-mail address to prevent spam-bots from sniffing it.
*
* @param string $email
* @return string
*/
public function email($email)
{
return str_replace('@', '@', $this->obfuscate($email));
}
/**
* ol generate an ordered list of items.
*
* @param array $list
* @param array $attributes
* @return string
*/
public function ol($list, $attributes = [])
{
return $this->listing('ol', $list, $attributes);
}
/**
* ul generates an un-ordered list of items.
*
* @param array $list
* @param array $attributes
* @return string
*/
public function ul($list, $attributes = [])
{
return $this->listing('ul', $list, $attributes);
}
/**
* listing HTML element.
*
* @param string $type
* @param array $list
* @param array $attributes
* @return string
*/
protected function listing($type, $list, $attributes = [])
{
$html = '';
if (count($list) === 0) {
return $html;
}
// Essentially we will just spin through the list and build the list of the HTML
// elements from the array. We will also handled nested lists in case that is
// present in the array. Then we will build out the final listing elements.
foreach ($list as $key => $value) {
$html .= $this->listingElement($key, $type, $value);
}
$attributes = $this->attributes($attributes);
return "<{$type}{$attributes}>{$html}{$type}>";
}
/**
* listingElement creates the HTML for a listing element.
*
* @param mixed $key
* @param string $type
* @param string $value
* @return string
*/
protected function listingElement($key, $type, $value)
{
if (is_array($value)) {
return $this->nestedListing($key, $type, $value);
}
return '
'.e($value).'
';
}
/**
* nestedListing creates the HTML for a nested listing attribute.
*
* @param mixed $key
* @param string $type
* @param string $value
* @return string
*/
protected function nestedListing($key, $type, $value)
{
if (is_int($key)) {
return $this->listing($type, $value);
}
return '
'.$key.$this->listing($type, $value).'
';
}
/**
* Build an HTML attribute string from an array.
*
* @param array $attributes
* @return string
*/
public function attributes($attributes)
{
$html = [];
// For numeric keys we will assume that the key and the value are the same
// as this will convert HTML attributes such as "required" to a correct
// form like required="required" instead of using incorrect numerics.
foreach ((array) $attributes as $key => $value) {
$element = $this->attributeElement($key, $value);
if (!is_null($element)) {
$html[] = $element;
}
}
return count($html) > 0 ? ' '.implode(' ', $html) : '';
}
/**
* attributeElement builds a single attribute element.
*
* @param string $key
* @param string $value
* @return string
*/
protected function attributeElement($key, $value)
{
if (is_numeric($key)) {
$key = $value;
}
if (is_null($value)) {
return;
}
if ($value === true) {
return $key;
}
elseif (is_array($value)) {
$value = substr(htmlspecialchars(json_encode($value), ENT_QUOTES, 'UTF-8'), 1, -1);
}
else {
$value = e($value);
}
return $key.'="'.$value.'"';
}
/**
* obfuscate a string to prevent spam-bots from sniffing it.
* @param string $value
* @return string
*/
public function obfuscate($value)
{
$safe = '';
foreach (str_split($value) as $letter) {
if (ord($letter) > 128) {
return $letter;
}
// To properly obfuscate the value, we will randomly convert each letter to
// its entity or hexadecimal representation, keeping a bot from sniffing
// the randomly obfuscated letters out of the string on the responses.
switch (rand(1, 3)) {
case 1:
$safe .= ''.ord($letter).';';
break;
case 2:
$safe .= ''.dechex(ord($letter)).';';
break;
case 3:
$safe .= $letter;
}
}
return $safe;
}
/**
* strip removes HTML from a string, with allowed tags, e.g.
* @param $string
* @param $allow
* @return string
*/
public static function strip($string, $allow = '')
{
return strip_tags(htmlspecialchars_decode($string), $allow);
}
/**
* limit HTML with specific length with a proper tag handling.
* @param string $html HTML string to limit
* @param int $maxLength String length to truncate at
* @param string $end
* @return string
*/
public static function limit($html, $maxLength = 100, $end = '...')
{
$isUtf8 = true;
$printedLength = 0;
$position = 0;
$tags = [];
$regex = $isUtf8
? '{?([a-z]+)[^>]*>|?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}'
: '{?([a-z]+)[^>]*>|?[a-zA-Z0-9]+;}';
$result = '';
while ($printedLength < $maxLength && preg_match($regex, $html, $match, PREG_OFFSET_CAPTURE, $position)) {
list($tag, $tagPosition) = $match[0];
$str = substr($html, $position, $tagPosition - $position);
if ($printedLength + strlen($str) > $maxLength) {
$result .= substr($str, 0, $maxLength - $printedLength) . $end;
$printedLength = $maxLength;
break;
}
$result .= $str;
$printedLength += strlen($str);
if ($printedLength >= $maxLength) {
$result .= $end;
break;
}
if ($tag[0] === '&' || ord($tag[0]) >= 0x80) {
$result .= $tag;
$printedLength++;
}
else {
$tagName = $match[1][0];
if ($tag[1] === '/') {
$openingTag = array_pop($tags);
$result .= $tag;
}
elseif ($tag[strlen($tag) - 2] === '/') {
$result .= $tag;
}
else {
$result .= $tag;
$tags[] = $tagName;
}
}
$position = $tagPosition + strlen($tag);
}
if ($printedLength < $maxLength && $position < strlen($html)) {
$result .= substr($html, $position, $maxLength - $printedLength);
}
while (!empty($tags)) {
$result .= sprintf('%s>', array_pop($tags));
}
return $result;
}
/**
* minify makes HTML more compact
*/
public static function minify($html)
{
$search = [
// Strip whitespaces after tags, except space
'/\>[^\S ]+/s',
// Strip whitespaces before tags, except space
'/[^\S ]+\/'
];
$replace = [
'>',
'<',
'\\1',
''
];
return preg_replace($search, $replace, $html);
}
/**
* clean HTML to prevent XSS attacks using DOM-based sanitization.
*/
public static function clean(string $html): string
{
$config = (new HtmlSanitizerConfig())
->allowSafeElements()
->allowRelativeLinks()
->allowRelativeMedias();
$sanitizer = new HtmlSanitizer($config);
return $sanitizer->sanitize($html);
}
/**
* cleanCss sanitizes CSS content to prevent injection attacks while preserving
* valid CSS syntax. Unlike clean() which is designed for HTML, this method handles
* CSS-specific threats: closing style tags, javascript: URLs, and legacy IE expressions.
*/
public static function cleanCss(string $css): string
{
// Strip any HTML tags (prevents injection)
$css = strip_tags($css);
// Remove CSS expressions and legacy IE behaviors
$css = preg_replace('/expression\s*\(/i', '(', $css);
$css = preg_replace('/behavior\s*:/i', '', $css);
$css = preg_replace('/-moz-binding\s*:/i', '', $css);
// Remove javascript: and vbscript: from url() values
$css = preg_replace('/url\s*\(\s*[\'"]?\s*(?:javascript|vbscript)\s*:/i', 'url(invalid:', $css);
return $css;
}
/**
* cleanVector sanitizes XML/SVG content to prevent XSS attacks using DOM-based sanitization.
* Uses enshrined/svg-sanitize library which is ported from DOMPurify.
*/
public static function cleanVector(string $html): string
{
$sanitizer = new SvgSanitizer();
$sanitizer->minify(false);
$sanitizer->removeRemoteReferences(true);
$sanitizer->removeXMLTag(true);
$clean = $sanitizer->sanitize($html);
return $clean !== false ? $clean : '';
}
/**
* isValidColor determines if a given string is a valid CSS color value
*/
public function isValidColor(string $value): bool
{
return Str::startsWith($value, [
'#',
'var(',
'rgb(',
'rgba(',
'hsl('
]);
}
}
================================================
FILE: src/Html/HtmlServiceProvider.php
================================================
registerHtmlBuilder();
$this->registerFormBuilder();
$this->registerBlockBuilder();
}
/**
* Register the HTML builder instance.
* @return void
*/
protected function registerHtmlBuilder()
{
$this->app->singleton('html', function ($app) {
return new HtmlBuilder($app['url']);
});
}
/**
* Register the form builder instance.
* @return void
*/
protected function registerFormBuilder()
{
$this->app->singleton('form', function ($app) {
$form = new FormBuilder($app['html'], $app['url'], $app['session.store']->token(), str_random(40));
return $form->setSessionStore($app['session.store']);
});
}
/**
* Register the Block builder instance.
* @return void
*/
protected function registerBlockBuilder()
{
$this->app->singleton('block', function ($app) {
return new BlockBuilder;
});
}
/**
* provides gets the services provided by the provider
*/
public function provides()
{
return ['html', 'form', 'block'];
}
}
================================================
FILE: src/Html/README.md
================================================
## Rain Html
An extension of `illuminate\html` and more.
### HTML helpers
These additional helpers are available in the `Helper` class.
**nameToArray**
Converts a HTML array string to a PHP array. Empty values are removed.
```php
// Converts to PHP array ['user', 'location', 'city']
$array = Helper::nameToArray('user[location][city]');
```
**strip**
Removes HTML from a string.
```php
// Outputs: Fatal Error! Oh noes!
echo Html::strip('Fatal Error! Oh noes!');
```
================================================
FILE: src/Html/UrlMixin.php
================================================
provider = $provider;
}
/**
* makeRelative converts a full URL to a relative URL
*/
public function makeRelative($url)
{
$fullUrl = $this->provider->to($url);
return parse_url($fullUrl, PHP_URL_PATH)
. (($query = parse_url($fullUrl, PHP_URL_QUERY)) ? '?' . $query : '')
. (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : '')
?: '/';
}
/**
* toRelative makes a link relative if configuration asks for it
*/
public function toRelative($url)
{
return Config::get('system.relative_links', false)
? $this->makeRelative($url)
: $this->provider->to($url);
}
/**
* toSigned signs a bare URL that can be validated with hasValidSignature
*/
public function toSigned($url, $expiration = null, $absolute = true)
{
if (!$absolute) {
$url = $this->makeRelative($url);
}
$parameters = [];
$parts = parse_url($url);
parse_str($parts['query'] ?? '', $parameters);
unset($parameters['signature']);
ksort($parameters);
if ($expiration) {
unset($parameters['expires']);
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}
$key = Config::get('app.key');
$signUrl = http_build_url($url, ['query' => http_build_query($parameters)]);
$signature = hash_hmac('sha256', $signUrl, $key);
return http_build_url($url, ['query' => http_build_query($parameters + ['signature' => $signature])]);
}
/**
* assetVersion takes a disk path, resolves it to a public URL, and appends
* a cache-busting version query string based on the file's modification time.
*
* Supports path symbols: ~ (base), $ (plugins), # (themes)
*
* Url::assetVersion('~/themes/demo/assets/js/app.js')
* // → http://localhost/themes/demo/assets/js/app.js?v1a2b3c4d
*/
public function assetVersion(string $path): string
{
// External URLs pass through unchanged
if (str_starts_with($path, '//') || str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
return $path;
}
// Already has a query string, skip versioning
if (str_contains($path, '?')) {
return $path;
}
// Resolve path symbols (~, $, #) to filesystem path
$filePath = File::symbolizePath($path);
// Ensure absolute filesystem path
if (!str_starts_with($filePath, base_path())) {
$filePath = base_path(ltrim($filePath, '/'));
}
// Compute version hash from file modification time
if (is_file($filePath)) {
$version = hash('crc32', (string) filemtime($filePath));
}
else {
$version = hash('crc32', (string) filemtime(base_path('vendor/autoload.php')));
}
// Convert disk path to public-facing relative path
$publicPath = $filePath;
$basePath = base_path();
if (str_starts_with($publicPath, $basePath)) {
$publicPath = substr($publicPath, strlen($basePath));
}
// Normalize directory separators for URL
$publicPath = str_replace('\\', '/', $publicPath);
// Generate public URL (respects app.asset_url for CDN/S3)
$url = $this->provider->asset($publicPath);
return $url . '?v' . $version;
}
}
================================================
FILE: src/Html/UrlServiceProvider.php
================================================
registerRelativeHelpers();
$this->registerRequestHelpers();
$this->registerUrlGeneratorPolicy();
$this->app['events']->listen('site.changed', function() {
$this->registerUrlGeneratorPolicy();
});
}
/**
* registerUrlGeneratorPolicy controls how URL links are generated throughout the application.
*
* detect - detect hostname and use the current schema
* secure - detect hostname and force HTTPS schema
* insecure - detect hostname and force HTTP schema
* force - force hostname and schema using app.url config value
*/
protected function registerUrlGeneratorPolicy()
{
$provider = $this->app['url'];
$policy = $this->app['config']->get('system.link_policy', 'detect');
$appUrl = $this->app['config']->get('app.url');
switch (strtolower($policy)) {
case 'force':
$provider->forceRootUrl($appUrl);
$provider->forceScheme(str_starts_with($appUrl, 'http://') ? 'http' : 'https');
break;
case 'insecure':
$provider->forceScheme('http');
break;
case 'secure':
$provider->forceScheme('https');
break;
}
// Workaround for October CMS installed to a subdirectory since
// Laravel won't support this use case, related issue:
// https://github.com/laravel/framework/pull/3918
if ($this->app->runningInConsole()) {
$provider->forceRootUrl($appUrl);
}
}
/**
* registerRelativeHelpers
*/
protected function registerRelativeHelpers()
{
$provider = $this->app['url'];
$provider->macro('makeRelative', function(...$args) use ($provider) {
return (new \October\Rain\Html\UrlMixin($provider))->makeRelative(...$args);
});
$provider->macro('toRelative', function(...$args) use ($provider) {
return (new \October\Rain\Html\UrlMixin($provider))->toRelative(...$args);
});
$provider->macro('toSigned', function(...$args) use ($provider) {
return (new \October\Rain\Html\UrlMixin($provider))->toSigned(...$args);
});
$provider->macro('assetVersion', function(...$args) use ($provider) {
return (new \October\Rain\Html\UrlMixin($provider))->assetVersion(...$args);
});
}
/**
* registerRequestHelpers
*/
protected function registerRequestHelpers()
{
$provider = $this->app['request'];
$provider->macro('pjaxCached', function() use ($provider) {
return $provider->headers->get('X-PJAX-CACHED') == true;
});
$provider->macro('isCrawler', function($userAgent = null) use ($provider) {
return (new \Jaybizzle\CrawlerDetect\CrawlerDetect($provider->server()))
->isCrawler($userAgent)
;
});
}
}
================================================
FILE: src/Installer/Console/OctoberBuild.php
================================================
output->section(Lang::get('system::lang.installer.dependencies_section'));
$this->setupInstallOctober();
$this->outputOutro();
}
/**
* getOptions get the console command options
*/
protected function getOptions()
{
return [
['want', 'w', InputOption::VALUE_REQUIRED, 'Provide a custom version.'],
];
}
}
================================================
FILE: src/Installer/Console/OctoberInstall.php
================================================
input->isInteractive()) {
return $this->handleNonInteractive();
}
if (!$this->checkEnvWritable()) {
$this->output->error('Cannot write to .env file. Check file permissions and try again.');
return 1;
}
$this->outputIntro();
$this->setupEncryptionKey();
$this->outputLanguageTable();
$this->setupLanguage();
if ($this->nonInteractiveCheck()) {
$this->outputNonInteractive();
return 1;
}
// Application Configuration
$this->output->section(Lang::get('system::lang.installer.app_config_section'));
$this->setupApplicationUrls();
$this->setupDatabaseConfig();
// Demo Theme
$this->output->section(Lang::get('system::lang.installer.demo_section'));
$this->setupDemoTheme();
// License Key
$this->output->section(Lang::get('system::lang.installer.license_section'));
$this->setupLicenseKey();
// Installing Dependencies
$this->output->section(Lang::get('system::lang.installer.dependencies_section'));
$this->setupInstallOctober();
// $this->output->section('Migrating Database');
// $this->refreshEnvVars();
// $this->setupMigrateDatabase();
$this->outputOutro();
}
/**
* handleNonInteractive processes the install without any prompts
*/
protected function handleNonInteractive()
{
$this->line('Installing October CMS (non-interactive)...');
$this->line('');
$errCode = null;
$this->comment('Migrating database...');
passthru('php artisan october:migrate', $errCode);
if ($errCode !== 0) {
$this->output->error('october:migrate failed.');
return 1;
}
$this->line('');
$this->comment('Migrating tailor...');
passthru('php artisan tailor:migrate', $errCode);
if ($errCode !== 0) {
$this->output->error('tailor:migrate failed.');
return 1;
}
$this->line('');
$this->comment('Seeding demo theme...');
passthru('php artisan theme:seed demo', $errCode);
if ($errCode !== 0) {
$this->output->error('theme:seed failed.');
return 1;
}
$this->line('');
$this->comment('Setting build number...');
passthru('php artisan october:util set build', $errCode);
if ($errCode !== 0) {
$this->output->error('october:util set build failed.');
return 1;
}
$this->line('');
$this->output->success('October CMS installed successfully.');
}
/**
* setupDemoTheme
*/
protected function setupDemoTheme()
{
// Install the demo theme and content?
$this->setDemoContent(
$this->confirm(Lang::get('system::lang.installer.install_demo_label'), true)
);
}
/**
* setupLanguage asks the user their language preference
*/
protected function setupLanguage()
{
try {
$locale = strtolower($this->ask(
// Select Language
Lang::get('system::lang.installer.locale_select_label'),
env('APP_LOCALE', 'en')
));
$availableLocales = $this->getAvailableLocales();
if (!isset($availableLocales[$locale])) {
throw new Exception("Language code '{$locale}' is invalid, please try again");
}
$this->setEnvVar('APP_LOCALE', $locale);
Lang::setLocale($locale);
}
catch (Exception $ex) {
$this->output->error($ex->getMessage());
return $this->setupLanguage();
}
}
/**
* outputLanguageTable displays a 2 column table with the available locales
*/
protected function outputLanguageTable()
{
$locales = $this->getAvailableLocales();
$tableRows = [];
$halfCount = count($locales) / 2;
$i = 0;
foreach ($locales as $locale => $info) {
if ($i < $halfCount) {
$tableRows[$i] = [$locale, "({$info[1]}) {$info[0]}"];
}
else {
$tableRows[$i-$halfCount] = array_merge(
$tableRows[$i-$halfCount],
[$locale, "({$info[1]}) {$info[0]}"]
);
}
$i++;
}
$this->output->table(['Code', 'Language', 'Code', 'Language'], $tableRows);
}
/**
* setupApplicationUrls asks for URL based configuration
*/
protected function setupApplicationUrls()
{
$url = $this->ask(
// Application URL
Lang::get('system::lang.installer.app_url_label'),
env('APP_URL', 'http://localhost')
);
$url = rtrim(trim($url), '/');
$this->setEnvVar('APP_URL', $url);
// To secure your application, use a custom address for accessing the admin panel.
$this->comment(Lang::get('system::lang.installer.backend_uri_comment'));
$backendUri = $this->ask(
// Backend URI
Lang::get('system::lang.installer.backend_uri_label'),
env('BACKEND_URI', '/admin')
);
if ($backendUri[0] !== '/') {
$backendUri = '/'.$backendUri;
}
$this->setEnvVar('BACKEND_URI', $backendUri);
}
/**
* setupEncryptionKey sets the application encryption key if not set already
*/
protected function setupEncryptionKey()
{
if (env('APP_KEY')) {
return;
}
$key = $this->getRandomKey();
$this->setEnvVar('APP_KEY', $key);
Config::set('app.key', $key);
$this->info(sprintf('Application key [%s] set successfully.', $key));
}
/**
* setupDatabaseConfig requests the database engine
*/
protected function setupDatabaseConfig()
{
$typeMap = [
'SQLite' => 'sqlite',
'MySQL' => 'mysql',
'Postgres' => 'pgsql',
'SQL Server' => 'sqlsrv',
];
$currentDriver = array_flip($typeMap)[$this->getEnvVar('DB_CONNECTION')] ?? 'SQLite';
$type = $this->choice(
// Database Engine
Lang::get('system::lang.installer.database_engine_label'),
[
'SQLite',
'MySQL',
'Postgres',
'SQL Server'
],
$currentDriver
);
$driver = $typeMap[$type] ?? 'sqlite';
$this->setEnvVar('DB_CONNECTION', $driver);
Config::set('database.default', $driver);
if ($driver === 'sqlite') {
$this->setupDatabaseSqlite();
}
else {
$this->setupDatabase($driver);
}
// Validate database connection
try {
$this->checkDatabaseFromConfig($driver);
}
catch (Exception $ex) {
$this->output->error($ex->getMessage());
return $this->setupDatabaseConfig();
}
}
/**
* checkDatabaseFromConfig validates the supplied database config
* and throws an exception if something is wrong
*/
protected function checkDatabaseFromConfig($driver)
{
$this->checkDatabase(
$driver,
Config::get("database.connections.{$driver}.host"),
Config::get("database.connections.{$driver}.port"),
Config::get("database.connections.{$driver}.database"),
Config::get("database.connections.{$driver}.username"),
Config::get("database.connections.{$driver}.password")
);
}
/**
* setupDatabase sets up a SQL based database
*/
protected function setupDatabase($driver)
{
// Hostname for the database connection.
$this->comment(Lang::get('system::lang.installer.database_host_comment'));
$host = $this->ask(
// Database Host
Lang::get('system::lang.installer.database_host_label'),
$this->getEnvVar('DB_HOST', 'localhost')
);
$this->setEnvVar('DB_HOST', $host);
Config::set("database.connections.{$driver}.host", $host);
// (Optional) A port for the connection.
$this->comment(Lang::get('system::lang.installer.database_port_comment'));
$port = $this->ask(
// Database Port
Lang::get('system::lang.installer.database_port_label'),
$this->getEnvVar('DB_PORT', false)
) ?: '';
$this->setEnvVar('DB_PORT', $port);
Config::set("database.connections.{$driver}.port", $port);
// Specify the name of the database to use.
$this->comment(Lang::get('system::lang.installer.database_name_comment'));
$database = $this->ask(
// Database Name
Lang::get('system::lang.installer.database_name_label'),
$this->getEnvVar('DB_DATABASE', 'octobercms')
);
$this->setEnvVar('DB_DATABASE', $database);
Config::set("database.connections.{$driver}.database", $database);
// User with create database privileges.
$this->comment(Lang::get('system::lang.installer.database_login_comment'));
$username = $this->ask(
// Database Login
Lang::get('system::lang.installer.database_login_label'),
$this->getEnvVar('DB_USERNAME', 'root')
);
$this->setEnvVar('DB_USERNAME', $username);
Config::set("database.connections.{$driver}.username", $username);
// Password for the specified user.
$this->comment(Lang::get('system::lang.installer.database_pass_comment'));
$password = $this->ask(
// Database Password
Lang::get('system::lang.installer.database_pass_label'),
$this->getEnvVar('DB_PASSWORD', false)
) ?: '';
$this->setEnvVar('DB_PASSWORD', $password);
Config::set("database.connections.{$driver}.password", $password);
}
/**
* setupDatabaseSqlite sets up the SQLite database engine
*/
protected function setupDatabaseSqlite()
{
// For file-based storage, enter a path relative to the application root directory.
$this->comment(Lang::get('system::lang.installer.database_path_comment'));
$defaultDb = $this->getEnvVar('DB_DATABASE', 'storage/database.sqlite');
if ($defaultDb === 'database') {
$defaultDb = 'storage/database.sqlite';
}
$filename = $this->ask(
// Database Path
Lang::get('system::lang.installer.database_path_label'),
$defaultDb
);
$this->setEnvVar('DB_DATABASE', $filename);
Config::set("database.connections.sqlite.database", $filename);
try {
if (!file_exists($filename)) {
$directory = dirname($filename);
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
new PDO('sqlite:'.$filename);
}
}
catch (Exception $ex) {
$this->output->error($ex->getMessage());
return $this->setupDatabaseSqlite();
}
return ['database' => $filename];
}
/**
* setupLicenseKey asks for the license key
*/
protected function setupLicenseKey()
{
if ($this->keyRetries++ > 10) {
// Too many failed attempts
$this->output->error(Lang::get('system::lang.installer.too_many_failures_label'));
$this->outputNonInteractive();
exit(1);
}
// Enter a valid License Key to proceed.
$this->comment(Lang::get('system::lang.installer.license_key_comment'));
// License Key
$licenseKey = trim($this->ask(Lang::get('system::lang.installer.license_key_label')));
if (!strlen($licenseKey)) {
return $this->setupLicenseKey();
}
try {
$this->setupSetProject($licenseKey);
// Thanks for being a customer of October CMS!
$this->output->success(Lang::get('system::lang.installer.license_thanks_comment'));
}
catch (Exception $ex) {
$this->output->error($ex->getMessage());
return $this->setupLicenseKey();
}
}
/**
* setupMigrateDatabase migrates the database
*/
protected function setupMigrateDatabase()
{
$errCode = null;
$exec = 'php artisan october:migrate';
$this->comment("Executing: {$exec}");
$this->line('');
passthru($exec, $errCode);
if ($errCode !== 0) {
$this->outputFailedOutro();
exit(1);
}
}
protected function outputNonInteractive()
{
// Too many failed attempts
$this->output->error(Lang::get('system::lang.installer.non_interactive_label'));
// If you see this error immediately, use these non-interactive commands instead.
$this->comment(Lang::get('system::lang.installer.non_interactive_comment'));
$this->line('');
// Open this application in your browser
$this->line(Lang::get('system::lang.installer.open_configurator_comment'));
$this->line('');
$this->line('-- OR --');
$this->line('');
$this->line("* php artisan project:set ");
$this->line('');
if ($want = $this->option('want')) {
$this->line("* php artisan october:build --want=".$want);
}
else {
$this->line("* php artisan october:build");
}
}
/**
* getOptions get the console command options
*/
protected function getOptions()
{
return [
['want', 'w', InputOption::VALUE_REQUIRED, 'Provide a custom version.'],
];
}
}
================================================
FILE: src/Installer/GatewayClient.php
================================================
setCredentials('your-api-key', 'your-api-secret');
* $projects = $client->listProjects();
*
* // Project-level operations (uses license key or auth.json hash)
* $client = (new GatewayClient)->setProjectHash('your-license-key-or-hash');
* $updates = $client->checkForUpdates(['plugins' => [...], 'themes' => [...]]);
*
*
*/
class GatewayClient
{
/**
* @var string API base URL
*/
const API_BASE_URL = 'https://api.octobercms.com';
/**
* @var string API version prefix
*/
const API_VERSION = 'v1';
/**
* @var string|null apiKey for HMAC authentication
*/
protected $apiKey;
/**
* @var string|null apiSecret for HMAC authentication
*/
protected $apiSecret;
/**
* @var string|null projectHash for project-level authentication (license key or auth.json hash)
*/
protected $projectHash;
/**
* @var int timeout in seconds
*/
protected $timeout = 30;
/**
* @var array lastResponseHeaders
*/
protected $lastResponseHeaders = [];
/**
* @var int lastStatusCode
*/
protected $lastStatusCode = 0;
/**
* @var string|null lastErrorCode from API response
*/
protected $lastErrorCode;
// =========================================================================
// CONFIGURATION
// =========================================================================
/**
* setCredentials for account-level authentication
*/
public function setCredentials(string $apiKey, string $apiSecret): self
{
$this->apiKey = $apiKey;
$this->apiSecret = $apiSecret;
return $this;
}
/**
* setProjectHash for project-level authentication
*
* @param string $projectHash License key or auth.json hash (bind code)
*/
public function setProjectHash(string $projectHash): self
{
$this->projectHash = $projectHash;
return $this;
}
/**
* setTimeout for requests
*/
public function setTimeout(int $seconds): self
{
$this->timeout = $seconds;
return $this;
}
// =========================================================================
// ACCOUNT-LEVEL ENDPOINTS (HMAC Auth Required)
// =========================================================================
/**
* createProject creates a new project
*
* @param string $name Project name
* @param array $options Optional: description
* @return array Project data including project_id and license_key
* @throws Exception
*/
public function createProject(string $name, array $options = []): array
{
return $this->requestWithHmac('projects/create', array_merge(
['name' => $name],
$options
));
}
/**
* updateProject updates an existing project
*
* @param int $projectId Project ID
* @param array $options Optional: name, description
* @return array Updated project data
* @throws Exception
*/
public function updateProject(int $projectId, array $options = []): array
{
return $this->requestWithHmac('projects/update', array_merge(
['project_id' => $projectId],
$options
));
}
/**
* deleteProject deletes a project
*
* @param int $projectId Project ID
* @return array Confirmation
* @throws Exception
*/
public function deleteProject(int $projectId): array
{
return $this->requestWithHmac('projects/delete', [
'project_id' => $projectId,
]);
}
/**
* listProjects returns all projects for the account
*
* @param int $limit Results per page (1-100)
* @param string|null $cursor Pagination cursor
* @return array Projects list with next_cursor
* @throws Exception
*/
public function listProjects(int $limit = 50, ?string $cursor = null): array
{
$params = ['limit' => $limit];
if ($cursor !== null) {
$params['cursor'] = $cursor;
}
return $this->requestWithHmac('projects/list', $params);
}
/**
* getProject returns detailed information about a project
*
* @param int $projectId Project ID
* @return array Project details
* @throws Exception
*/
public function getProject(int $projectId): array
{
return $this->requestWithHmac('projects/get', [
'project_id' => $projectId,
]);
}
/**
* lookupByDomain looks up a project by domain name
*
* @param string $domainName Domain to look up
* @return array Project ID
* @throws Exception
*/
public function lookupByDomain(string $domainName): array
{
return $this->requestWithHmac('licenses/domain', [
'domain_name' => $domainName,
]);
}
/**
* rotateLicense regenerates a license key
*
* @param int $projectId Project ID
* @return array New license_key and license_hash
* @throws Exception
*/
public function rotateLicense(int $projectId): array
{
return $this->requestWithHmac('licenses/rotate', [
'project_id' => $projectId,
]);
}
/**
* attachPackage attaches a package to a project
*
* @param int $projectId Project ID
* @param string $packageCode Package code (e.g., 'Author.PluginName')
* @param string $type Package type ('plugin' or 'theme')
* @return array Confirmation with attached, package, type
* @throws Exception
*/
public function attachPackage(int $projectId, string $packageCode, string $type): array
{
return $this->requestWithHmac('projects/packages/attach', [
'project_id' => $projectId,
'package' => $packageCode,
'type' => $type,
]);
}
/**
* detachPackage detaches a package from a project
*
* @param int $projectId Project ID
* @param string $packageCode Package code (e.g., 'Author.PluginName')
* @param string $type Package type ('plugin' or 'theme')
* @return array Confirmation with detached, package, type
* @throws Exception
*/
public function detachPackage(int $projectId, string $packageCode, string $type): array
{
return $this->requestWithHmac('projects/packages/detach', [
'project_id' => $projectId,
'package' => $packageCode,
'type' => $type,
]);
}
// =========================================================================
// PROJECT-LEVEL ENDPOINTS
// =========================================================================
/**
* getProjectDetail returns project details (project-level auth)
*
* @param string|null $projectHash Optional project hash override
* @return array Project details
* @throws Exception
*/
public function getProjectDetail(?string $projectHash = null): array
{
return $this->requestWithProject('project/detail', [
'id' => $projectHash ?? $this->projectHash,
]);
}
/**
* checkForUpdates checks for available updates (project-level auth)
*
* @param array $options plugins (assoc array code=>version), themes, version, build
* @return array Available updates
* @throws Exception
*/
public function checkForUpdates(array $options = []): array
{
$params = [];
if (isset($options['plugins'])) {
$params['plugins'] = base64_encode(json_encode($options['plugins']));
}
if (isset($options['themes'])) {
$params['themes'] = base64_encode(json_encode($options['themes']));
}
if (isset($options['version'])) {
$params['version'] = $options['version'];
}
if (isset($options['build'])) {
$params['build'] = $options['build'];
}
return $this->requestWithProject('project/check', $params);
}
/**
* getPackages returns multiple package details
*
* @param array $codes Package codes
* @param string $type Package type (plugin or theme)
* @return array Package information
* @throws Exception
*/
public function getPackages(array $codes, string $type = 'plugin'): array
{
return $this->request('package/details', [
'names' => $codes,
'type' => $type,
]);
}
/**
* getPackage returns single package details
*
* @param string $code Package code
* @param string $type Package type
* @return array Package information with requirements
* @throws Exception
*/
public function getPackage(string $code, string $type = 'plugin'): array
{
return $this->requestWithProject('package/detail', [
'name' => $code,
'type' => $type,
]);
}
/**
* getPackageContent returns package documentation content
*
* @param string $code Package code
* @param string $type Package type
* @return array Package info with HTML content
* @throws Exception
*/
public function getPackageContent(string $code, string $type = 'plugin'): array
{
return $this->requestWithProject('package/content', [
'name' => $code,
'type' => $type,
]);
}
/**
* browsePackages returns a paginated list of packages
*
* @param int $page Page number
* @param string $type Package type
* @param int|null $version Compatibility version filter
* @return array Paginated package list
* @throws Exception
*/
public function browsePackages(int $page = 1, string $type = 'plugin', ?int $version = null): array
{
$params = [
'page' => $page,
'type' => $type,
];
if ($version !== null) {
$params['version'] = $version;
}
return $this->request('package/browse', $params);
}
/**
* searchPackages searches for packages
*
* @param string $query Search query
* @param string|null $type Package type filter
* @return array Matching packages
* @throws Exception
*/
public function searchPackages(string $query, ?string $type = null): array
{
$params = ['query' => $query];
if ($type !== null) {
$params['type'] = $type;
}
return $this->request('package/search', $params);
}
/**
* getInstallDetail returns installation details
*
* @return array Core and package hashes
* @throws Exception
*/
public function getInstallDetail(): array
{
return $this->requestWithProject('install/detail', []);
}
// =========================================================================
// HTTP CLIENT
// =========================================================================
/**
* requestWithHmac makes a request with HMAC authentication
*/
protected function requestWithHmac(string $endpoint, array $params): array
{
if (!$this->apiKey || !$this->apiSecret) {
throw new Exception('API credentials required for this operation');
}
// Add nonce
$params['nonce'] = (string) round(microtime(true) * 1000);
// Build query string for signing
$queryString = http_build_query($params, '', '&');
// Compute signature
$signature = base64_encode(
hash_hmac('sha512', $queryString, base64_decode($this->apiSecret), true)
);
$headers = [
'Rest-Key: ' . $this->apiKey,
'Rest-Sign: ' . $signature,
];
return $this->request($endpoint, $params, 'POST', $headers);
}
/**
* requestWithProject makes a request with project-level authentication
*/
protected function requestWithProject(string $endpoint, array $params): array
{
$headers = [];
if ($this->projectHash) {
$headers[] = 'php-auth-pw: ' . $this->projectHash;
$params['project'] = $this->projectHash;
}
return $this->request($endpoint, $params, 'POST', $headers);
}
/**
* request makes an HTTP request
*
* @param string $endpoint API endpoint
* @param array $params Request parameters
* @param string $method HTTP method
* @param array $headers Additional headers
* @return array Decoded response
* @throws Exception
*/
protected function request(string $endpoint, array $params = [], string $method = 'POST', array $headers = []): array
{
$url = self::API_BASE_URL . '/' . self::API_VERSION . '/' . ltrim($endpoint, '/');
// Default headers
$headers = array_merge([
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
], $headers);
// Initialize cURL
$ch = curl_init();
if ($method === 'GET') {
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
}
else {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params, '', '&'));
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_HEADER => true,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
throw new Exception('cURL error: ' . $error);
}
$this->lastStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
// Parse headers and body
$headerString = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
$this->lastResponseHeaders = $this->parseHeaders($headerString);
$this->lastErrorCode = null;
// Decode JSON response
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid JSON response: ' . $body);
}
// Check for error responses
if ($this->lastStatusCode >= 400) {
$this->lastErrorCode = $data['error'] ?? 'unknown_error';
$message = $data['message'] ?? 'An error occurred';
throw new Exception($message, $this->lastStatusCode);
}
return $data;
}
/**
* parseHeaders extracts headers from response
*/
protected function parseHeaders(string $headerString): array
{
$headers = [];
foreach (explode("\r\n", $headerString) as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$headers[trim($key)] = trim($value);
}
}
return $headers;
}
/**
* getLastResponseSignature returns the last response signature for verification
*/
public function getLastResponseSignature(): ?string
{
return $this->lastResponseHeaders['Rest-Sign'] ?? null;
}
/**
* getLastStatusCode returns the last HTTP status code
*/
public function getLastStatusCode(): int
{
return $this->lastStatusCode;
}
/**
* getRetryAfter returns the Retry-After header value (for rate limiting)
*/
public function getRetryAfter(): ?int
{
$value = $this->lastResponseHeaders['Retry-After'] ?? null;
return $value !== null ? (int) $value : null;
}
/**
* getLastErrorCode returns the API error code from the last failed request
*/
public function getLastErrorCode(): ?string
{
return $this->lastErrorCode;
}
/**
* isRateLimited checks if the last error was a rate limit error
*/
public function isRateLimited(): bool
{
return $this->lastErrorCode === 'rate_limited' || $this->lastStatusCode === 429;
}
/**
* isAuthError checks if the last error was an authentication error
*/
public function isAuthError(): bool
{
return $this->lastErrorCode === 'invalid_auth' || $this->lastStatusCode === 401;
}
/**
* isValidationError checks if the last error was a validation error
*/
public function isValidationError(): bool
{
return $this->lastErrorCode === 'validation_error' || $this->lastStatusCode === 422;
}
}
================================================
FILE: src/Installer/InstallEventHandler.php
================================================
listen('backend.page.beforeDisplay', [static::class, 'extendPageDisplay']);
}
/**
* extendPageDisplay
*/
public function extendPageDisplay($controller, $action, $params)
{
if (System::checkProjectValid(1|32)) {
$controller->addJs('/modules/backend/assets/js/onboarding.js');
}
if (mt_rand(1, 64) === 1) {
$this->checkProjectState();
}
}
/**
* checkProjectState
*/
protected function checkProjectState()
{
return ($since = Parameter::getDate('system::core.since')) && $since->addMonths(3)->isPast()
? Parameter::set('system::project.is_stale', true)
: Parameter::setDate('system::core.since');
}
}
================================================
FILE: src/Installer/InstallManager.php
================================================
requestServerData('project/detail', ['id' => $projectId]);
}
/**
* getComposerUrl returns the endpoint for composer
*/
public function getComposerUrl(bool $withProtocol = true): string
{
$gateway = env('APP_COMPOSER_GATEWAY', Config::get('system.composer_gateway', 'gateway.octobercms.com'));
return $withProtocol ? 'https://'.$gateway : $gateway;
}
/**
* requestServerData contacts the update server for a response.
* @param string $uri
* @param array $postData
* @return array
*/
public function requestServerData($uri, $postData = [])
{
$result = $this->makeHttpRequest($this->createServerUrl($uri), $postData);
$contents = $result->body();
if ($result->status() === 404) {
throw new ApplicationException(Lang::get('system::lang.server.response_not_found'));
}
if ($result->status() !== 200) {
throw new ApplicationException(
strlen($contents)
? $contents
: Lang::get('system::lang.server.response_empty')
);
}
$resultData = false;
try {
$resultData = @json_decode($contents, true);
}
catch (Exception $ex) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
return $resultData;
}
/**
* createServerUrl creates a complete gateway server URL from supplied URI
* @param string $uri
* @return string
*/
protected function createServerUrl($uri)
{
$gateway = Config::get('system.update_gateway', 'https://gateway.octobercms.com/api');
if (substr($gateway, -1) != '/') {
$gateway .= '/';
}
return $gateway . $uri;
}
/**
* makeHttpRequest makes a specialized server request to a URL.
* @param string $url
* @param array $postData
* @return \Illuminate\Http\Client\Response
*/
protected function makeHttpRequest($url, $postData)
{
// New HTTP instance
$http = Http::asForm();
// Post data
$postData['protocol_version'] = '2.0';
$postData['client'] = 'October CMS';
$postData['server'] = base64_encode(json_encode([
'php' => PHP_VERSION,
'url' => Url::to('/'),
'since' => date('c')
]));
// Gateway auth
if ($credentials = Config::get('system.update_gateway_auth')) {
if (is_string($credentials)) {
$credentials = explode(':', $credentials);
}
list($user, $pass) = $credentials;
$http->withBasicAuth($user, $pass);
}
return $http->post($url, $postData);
}
}
================================================
FILE: src/Installer/Traits/SetupBuilder.php
================================================
setOutputCommand($this, $this->input);
$this->composerRequireCore($composer, $this->option('want') ?: null);
$this->line('');
}
/**
* composerRequireString returns the composer require string for installing dependencies
*/
protected function composerRequireCore($composer, $want = null)
{
if ($want === null) {
$composer->require(['october/all' => $this->getUpdateWantVersion()]);
}
else {
$want = $this->processWantString($want);
$composer->require([
'october/rain' => $want,
'october/all' => $want
]);
}
}
/**
* setupSetProject
*/
protected function setupSetProject($licenseKey)
{
$result = InstallManager::instance()->requestProjectDetails($licenseKey);
// Check status
$isActive = $result['is_active'] ?? false;
if (!$isActive) {
// License is unpaid or has expired. Please visit octobercms.com to obtain a license.
throw new Exception(Lang::get('system::lang.installer.license_expired_comment'));
}
// Configure composer and save authentication token
$this->setComposerAuth(
$result['email'] ?? null,
$result['project_id'] ?? null
);
}
/**
* outputIntro displays the introduction output
*/
protected function outputIntro()
{
$message = [
".=================================================.",
" ____ _____ _______ ____ ____ ___________ ",
" / __ \ / ____|__ __|/ __ \| _ \| ____| __ \ ",
" | | | | | | | | | | | |_) | |__ | |__) |",
" | | | | | | | | | | | _ <| __| | _ / ",
" | |__| | |____ | | | |__| | |_) | |____| | \ \ ",
" \____/ \_____| |_| \____/|____/|______|_| \_\\",
" ",
"`================== INSTALLATION =================' ",
"",
];
$this->line($message);
}
/**
* outputOutro displays the credits output
*/
protected function outputOutro()
{
$message = [
".~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.",
" ,@@@@@@@, ",
" ,,,. ,@@@@@@/@@, .oo8888o. ",
" ,&%%&%&&%,@@@@@/@@@@@@,8888\88/8o ",
" ,%&\%&&%&&%,@@@\@@@/@@@88\88888/88' ",
" %&&%&%&/%&&%@@\@@/ /@@@88888\88888' ",
" %&&%/ %&%%&&@@\ V /@@' `88\8 `/88' ",
" `&%\ ` /%&' |.| \ '|8' ",
" |o| | | | | ",
" |.| | | | | ",
"`========= INSTALLATION COMPLETE ========='",
"",
];
$this->line($message);
// Please migrate the database with the following command
$this->comment(Lang::get('system::lang.installer.migrate_database_comment'));
$this->line('');
$this->line("* php artisan october:migrate");
$this->line('');
$adminUrl = $this->getEnvVar('APP_URL') . $this->getEnvVar('BACKEND_URI');
// Then, open the administration area at this URL
$this->comment(Lang::get('system::lang.installer.visit_backend_comment'));
$this->line('');
$this->line("* {$adminUrl}");
}
/**
* outputFailedOutro displays the failure message
*/
protected function outputFailedOutro()
{
// Installation Failed
$this->output->title(Lang::get('system::lang.installer.install_failed_label'));
// Please try running these commands manually.
$this->output->error(Lang::get('system::lang.installer.install_failed_comment'));
$this->line('');
// Open this application in your browser
$this->line(Lang::get('system::lang.installer.open_configurator_comment'));
$this->line('');
$this->line('-- OR --');
$this->line('');
$this->line("* php artisan project:set ");
$this->line('');
if ($want = $this->option('want')) {
$this->line("* php artisan october:build --want=".$want);
}
else {
$this->line("* php artisan october:build");
}
}
/**
* nonInteractiveCheck will make a calculated guess if the command is running
* in non interactive mode by how long it takes to execute
*/
protected function nonInteractiveCheck(): bool
{
return (microtime(true) - LARAVEL_START) < 1;
}
}
================================================
FILE: src/Installer/Traits/SetupHelper.php
================================================
addAuthCredentials(
$this->getComposerUrl(false),
$email,
$projectKey
);
// Store project details
$this->injectJsonToFile(storage_path('cms/project.json'), [
'project' => $projectKey
]);
// Add gateway as a composer repo
$composer->addOctoberRepository($this->getComposerUrl());
}
/**
* setDemoContent instructs the system to install demo content or not
*/
protected function setDemoContent($confirm = true)
{
if ($confirm) {
$this->injectJsonToFile(storage_path('cms/autoexec.json'), [
'theme:seed demo --root'
]);
}
else {
$this->injectJsonToFile(storage_path('cms/autoexec.json'), [
'october:fresh --force'
]);
}
}
/**
* processWantString ensures a valid want version is supplied
*/
protected function processWantString($version)
{
$parts = explode('.', $version);
if (count($parts) > 1) {
$parts[2] = '*';
}
$parts = array_slice($parts, 0, 3);
return implode('.', $parts);
}
/**
* addModulesToGitignore
*/
protected function addModulesToGitignore($gitignore)
{
$toIgnore = '/modules';
$contents = file_get_contents($gitignore);
if (!preg_match('/^\/modules$/m', $contents)) {
file_put_contents(
$gitignore,
trim($contents, PHP_EOL) .
PHP_EOL .
$toIgnore .
PHP_EOL
);
}
}
/**
* setEnvVars sets multiple environment variables
*/
protected function setEnvVars(array $vars)
{
foreach ($vars as $key => $val) {
$this->setEnvVar($key, $val);
}
}
/**
* setEnvVar writes an environment variable to disk
*/
protected function setEnvVar($key, $value)
{
$path = base_path('.env');
$old = $this->getEnvVar($key);
$value = $this->encodeEnvVar($value);
if (is_bool(env($key))) {
$old = env($key) ? 'true' : 'false';
}
if (file_exists($path)) {
file_put_contents($path, str_replace(
[$key.'='.$old, $key.'='.'"'.$old.'"'],
[$key.'='.$value, $key.'='.$value],
file_get_contents($path)
));
}
$this->userConfig[$key] = $value;
}
/**
* encodeEnvVar for compatibility with certain characters
*/
protected function encodeEnvVar($value)
{
if (!is_string($value)) {
return $value;
}
// Escape quotes
if (strpos($value, '"') !== false) {
$value = str_replace('"', '\"', $value);
}
// Quote values with comment, space, quotes
$triggerChars = ['#', ' ', '"', "'"];
foreach ($triggerChars as $char) {
if (strpos($value, $char) !== false) {
$value = '"'.$value.'"';
break;
}
}
return $value;
}
/**
* getEnvVar specifically from installer specified values. This is needed since
* the writing to the environment file may not update the values from env()
*/
protected function getEnvVar(string $key, $default = null)
{
return $this->userConfig[$key] ?? env($key, $default);
}
/**
* checkDatabase validates the supplied database configuration
*/
protected function checkDatabase($type, $host, $port, $name, $user, $pass)
{
if ($type != 'sqlite' && !strlen($host)) {
throw new Exception('Please specify a database host');
}
if (!strlen($name)) {
throw new Exception('Please specify the database name');
}
// Check connection
switch ($type) {
case 'mysql':
$dsn = 'mysql:host='.$host.';dbname='.$name;
if ($port) {
$dsn .= ";port=".$port;
}
break;
case 'pgsql':
$_host = ($host) ? 'host='.$host.';' : '';
$dsn = 'pgsql:'.$_host.'dbname='.$name;
if ($port) {
$dsn .= ";port=".$port;
}
break;
case 'sqlite':
$dsn = 'sqlite:'.$name;
$this->checkSqliteFile($name);
break;
case 'sqlsrv':
$availableDrivers = PDO::getAvailableDrivers();
$portStr = $port ? ','.$port : '';
if (in_array('dblib', $availableDrivers)) {
$dsn = 'dblib:host='.$host.$portStr.';dbname='.$name;
}
else {
$dsn = 'sqlsrv:Server='.$host.$portStr.';Database='.$name;
}
break;
}
try {
return new PDO($dsn, $user, $pass, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
}
catch (PDOException $ex) {
throw new Exception('Connection failed: ' . $ex->getMessage());
}
}
/**
* validateSqliteFile will establish the SQLite engine
*/
protected function checkSqliteFile($filename)
{
if (file_exists($filename)) {
return;
}
$directory = dirname($filename);
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
new PDO('sqlite:'.$filename);
}
/**
* injectJsonToFile merges a JSON array in to an existing JSON file.
* Merging is useful for preserving array values.
*/
protected function injectJsonToFile(string $filename, array $jsonArr, bool $merge = false): void
{
$contentsArr = file_exists($filename)
? json_decode(file_get_contents($filename), true)
: [];
$newArr = $merge
? array_merge_recursive($contentsArr, $jsonArr)
: $this->mergeRecursive($contentsArr, $jsonArr);
$content = json_encode($newArr, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
file_put_contents($filename, $content);
}
/**
* mergeRecursive substitutes the native PHP array_merge_recursive to be
* more config friendly. Scalar values are replaced instead of being
* merged in to their own new array.
*/
protected function mergeRecursive(array $array1, $array2)
{
if ($array2 && is_array($array2)) {
foreach ($array2 as $key => $val2) {
if (
is_array($val2) &&
(($val1 = isset($array1[$key]) ? $array1[$key] : null) !== null) &&
is_array($val1)
) {
$array1[$key] = $this->mergeRecursive($val1, $val2);
}
else {
$array1[$key] = $val2;
}
}
}
return $array1;
}
/**
* getAvailableLocales returns available system locales
*/
public function getAvailableLocales()
{
return [
'ar' => [$this->getLang('system::lang.locale.ar'), 'Arabic'],
'be' => [$this->getLang('system::lang.locale.be'), 'Belarusian'],
'bg' => [$this->getLang('system::lang.locale.bg'), 'Bulgarian'],
'ca' => [$this->getLang('system::lang.locale.ca'), 'Catalan'],
'cs' => [$this->getLang('system::lang.locale.cs'), 'Czech'],
'da' => [$this->getLang('system::lang.locale.da'), 'Danish'],
'de' => [$this->getLang('system::lang.locale.de'), 'German'],
'el' => [$this->getLang('system::lang.locale.el'), 'Greek'],
'en' => [$this->getLang('system::lang.locale.en'), 'English'],
'en-au' => [$this->getLang('system::lang.locale.en-au'), 'English'],
'en-ca' => [$this->getLang('system::lang.locale.en-ca'), 'English'],
'en-gb' => [$this->getLang('system::lang.locale.en-gb'), 'English'],
'es' => [$this->getLang('system::lang.locale.es'), 'Spanish'],
'es-ar' => [$this->getLang('system::lang.locale.es-ar'), 'Spanish'],
'et' => [$this->getLang('system::lang.locale.et'), 'Estonian'],
'fa' => [$this->getLang('system::lang.locale.fa'), 'Persian'],
'fi' => [$this->getLang('system::lang.locale.fi'), 'Finnish'],
'fr' => [$this->getLang('system::lang.locale.fr'), 'French'],
'fr-ca' => [$this->getLang('system::lang.locale.fr-ca'), 'French'],
'hu' => [$this->getLang('system::lang.locale.hu'), 'Hungarian'],
'id' => [$this->getLang('system::lang.locale.id'), 'Indonesian'],
'it' => [$this->getLang('system::lang.locale.it'), 'Italian'],
'ja' => [$this->getLang('system::lang.locale.ja'), 'Japanese'],
'ko' => [$this->getLang('system::lang.locale.ko'), 'Korean'],
'lt' => [$this->getLang('system::lang.locale.lt'), 'Lithuanian'],
'lv' => [$this->getLang('system::lang.locale.lv'), 'Latvian'],
'nb-no' => [$this->getLang('system::lang.locale.nb-no'), 'Norwegian'],
'nl' => [$this->getLang('system::lang.locale.nl'), 'Dutch'],
'pl' => [$this->getLang('system::lang.locale.pl'), 'Polish'],
'pt-br' => [$this->getLang('system::lang.locale.pt-br'), 'Portuguese'],
'pt-pt' => [$this->getLang('system::lang.locale.pt-pt'), 'Portuguese'],
'ro' => [$this->getLang('system::lang.locale.ro'), 'Romanian'],
'ru' => [$this->getLang('system::lang.locale.ru'), 'Russian'],
'sk' => [$this->getLang('system::lang.locale.sk'), 'Slovak'],
'sl' => [$this->getLang('system::lang.locale.sl'), 'Slovene'],
'sv' => [$this->getLang('system::lang.locale.sv'), 'Swedish'],
'th' => [$this->getLang('system::lang.locale.th'), 'Thai'],
'tr' => [$this->getLang('system::lang.locale.tr'), 'Turkish'],
'uk' => [$this->getLang('system::lang.locale.uk'), 'Ukrainian'],
'vn' => [$this->getLang('system::lang.locale.vn'), 'Vietnamese'],
'zh-cn' => [$this->getLang('system::lang.locale.zh-cn'), 'Chinese'],
'zh-tw' => [$this->getLang('system::lang.locale.zh-tw'), 'Chinese'],
];
}
//
// Framework Booted
//
/**
* getRandomKey generates a random application key
*/
protected function getRandomKey(): string
{
return Str::random($this->getKeyLength(Config::get('app.cipher')));
}
/**
* getKeyLength returns the supported length of a key for a cipher
*/
protected function getKeyLength(string $cipher): int
{
return $cipher === 'AES-128-CBC' ? 16 : 32;
}
/**
* checkEnvWritable checks to see if the app can write to the .env file
*/
protected function checkEnvWritable()
{
$path = base_path('.env');
$gitignore = base_path('.gitignore');
// Copy environment variables and reload
if (!file_exists($path)) {
copy(base_path('.env.example'), $path);
$this->refreshEnvVars();
}
// Add modules to .gitignore
if (file_exists($gitignore) && is_writable($gitignore)) {
$this->addModulesToGitignore($gitignore);
}
return is_writable($path);
}
/**
* getComposerUrl returns the endpoint for composer
*/
protected function getComposerUrl(bool $withProtocol = true): string
{
return InstallManager::instance()->getComposerUrl($withProtocol);
}
/**
* refreshEnvVars will reload defined environment variables
*/
protected function refreshEnvVars()
{
DotEnv::create(Env::getRepository(), App::environmentPath(), App::environmentFile())->load();
}
}
================================================
FILE: src/Mail/FakeMailer.php
================================================
buildMailable($view, $data, $callback);
}
parent::send($view, $data = [], $callback = null);
}
/**
* queue a new e-mail message for sending
*/
public function queue($view, $data = null, $callback = null, $queue = null)
{
if (!$view instanceof Mailable) {
$view = $this->buildMailable($view, $data, $callback, true);
}
return parent::queue($view, $data = null, $callback = null, $queue = null);
}
/**
* mailablesOf a given type
*/
protected function mailablesOf($type): Collection
{
return collect($this->mailables)->filter(function ($mailable) use ($type) {
return $mailable->view === $type;
});
}
/**
* queuedMailablesOf of a given type
*/
protected function queuedMailablesOf($type): Collection
{
return collect($this->queuedMailables)->filter(function ($mailable) use ($type) {
return $mailable->view === $type;
});
}
/**
* buildMailable from a view file
*/
public function buildMailable($view, $data, $callback, $queued = false)
{
$mailable = new Mailable;
$mailable->locale('en');
$mailable->siteContext(1);
if ($queued) {
$mailable->view($view)->withSerializedData($data);
}
else {
$mailable->view($view, $data);
}
if ($callback !== null) {
call_user_func($callback, $mailable);
}
return $mailable;
}
}
================================================
FILE: src/Mail/MailManager.php
================================================
app['events']->dispatch('mailer.beforeResolve', [$this, $name]);
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Mailer [{$name}] is not defined.");
}
// Once we have created the mailer instance we will set a container instance
// on the mailer. This allows us to resolve mailer classes via containers
// for maximum testability on said classes instead of passing Closures.
$mailer = new Mailer(
$name,
$this->app['view'],
$this->createSymfonyTransport($config),
$this->app['events']
);
if ($this->app->bound('queue')) {
$mailer->setQueue($this->app['queue']);
}
// Next we will set all of the global addresses on this mailer, which allows
// for easy unification of all "from" addresses as well as easy debugging
// of sent messages since these will be sent to a single email address.
foreach (['from', 'reply_to', 'to', 'return_path'] as $type) {
$this->setGlobalAddress($mailer, $config, $type);
}
// Extensibility
$this->app['events']->dispatch('mailer.resolve', [$this, $name, $mailer]);
return $mailer;
}
}
================================================
FILE: src/Mail/MailParser.php
================================================
* Settings section
* ==
* Plain-text content section
* ==
* HTML content section
* ==
* CSS stylesheet section (layouts)
*
* If the content has only 2 sections they are considered as settings and HTML.
* If there is only a single section, it is considered as HTML.
* @param string $content Specifies the file content.
* @return array Returns an array with the following indexes: 'settings', 'html', 'text'.
* The 'html' and 'text' elements contain strings. The 'settings' element contains the
* parsed INI file as array. If the content string doesn't contain a section, the corresponding
* result element has null value.
*/
public static function parse($content)
{
$sections = preg_split('/^={2,}\s*/m', $content, -1);
$count = count($sections);
foreach ($sections as &$section) {
$section = trim($section);
}
$result = [
'settings' => [],
'html' => null,
'text' => null,
'css' => null
];
if ($count >= 4) {
$result['settings'] = Ini::parse($sections[0]);
$result['text'] = $sections[1];
$result['html'] = $sections[2];
$result['css'] = $sections[3];
}
elseif ($count >= 3) {
$result['settings'] = Ini::parse($sections[0]);
$result['text'] = $sections[1];
$result['html'] = $sections[2];
}
elseif ($count === 2) {
$result['settings'] = Ini::parse($sections[0]);
$result['html'] = $sections[1];
}
elseif ($count === 1) {
$result['html'] = $sections[0];
}
return $result;
}
}
================================================
FILE: src/Mail/MailServiceProvider.php
================================================
app->singleton('mail.manager', function ($app) {
// @deprecated use mailer.beforeResolve or callBeforeResolving
$this->app['events']->dispatch('mailer.beforeRegister', [$this]);
// Inheritance
$manager = new MailManager($app);
// @deprecated use mailer.resolve or callAfterResolving
$this->app['events']->dispatch('mailer.register', [$this, $manager]);
return $manager;
});
$this->app->bind('mailer', function ($app) {
return $app->make('mail.manager')->mailer();
});
}
}
================================================
FILE: src/Mail/Mailable.php
================================================
viewData;
foreach ($data as $param => $value) {
$data[$param] = $this->getRestoredPropertyValue($value);
}
return $data;
}
/**
* Set serialized view data for the message.
*
* @param array $data
* @return $this
*/
public function withSerializedData($data)
{
$this->viewData['_current_locale'] = $this->locale ?: App::getLocale();
$this->viewData['_current_site'] = $this->siteContext ?: Site::getSiteIdFromContext();
foreach ($data as $param => $value) {
$this->viewData[$param] = $this->getSerializedPropertyValue($value);
}
return $this;
}
/**
* Set the subject for the message.
*
* @param \Illuminate\Mail\Message $message
* @return $this
*/
protected function buildSubject($message)
{
if ($this->subject) {
$message->subject($this->subject);
}
return $this;
}
/**
* siteContext sets the site context of the message.
*
* @param string $siteId
* @return $this
*/
public function siteContext($siteId)
{
$this->siteContext = $siteId;
return $this;
}
/**
* withLocale acts as a hook to also apply the site context
*
* @param string $locale
* @param \Closure $callback
* @return mixed
*/
public function withLocale($locale, $callback)
{
if (!$this->siteContext) {
return parent::withLocale($locale, $callback);
}
return Site::withContext($this->siteContext, function() use ($locale, $callback) {
return parent::withLocale($locale, $callback);
});
}
/**
* forceMailer forces sending using a different mail driver, useful if lazy loading
* the mail driver configuration for multisite.
* @param string $mailer
* @return $this
*/
public function forceMailer($mailer)
{
$this->forceMailer = $mailer;
return $this;
}
/**
* mailer sets the name of the mailer that should send the message.
* @param string $mailer
* @return $this
*/
public function mailer($mailer)
{
$this->mailer = $this->forceMailer ?: $mailer;
return $this;
}
}
================================================
FILE: src/Mail/Mailer.php
================================================
bindEvent('mailer.beforeSend', function ((string|array) $view, (array) $data, (\Closure|string) $callback) {
* return false;
* });
*
*/
if (
($this->fireEvent('mailer.beforeSend', [$view, $data, $callback], true) === false) ||
(Event::fire('mailer.beforeSend', [$view, $data, $callback], true) === false)
) {
return;
}
if ($view instanceof MailableContract) {
return $this->sendMailable($view);
}
// Inheriting logic from Illuminate\Mail\Mailer...
// First we need to parse the view, which could either be a string or an array
// containing both an HTML and plain text versions of the view which should
// be used when sending an e-mail. We will extract both of them out here.
list($view, $plain, $raw) = $this->parseView($view);
$data['message'] = $message = $this->createMessage();
if (!is_null($callback)) {
$callback($message);
}
if (is_bool($raw) && $raw === true) {
$this->addContentRaw($message, $view, $plain);
}
else {
$this->addContent($message, $view, $plain, $raw, $data);
}
// If a global "to" address has been set, we will set that address on the mail
// message. This is primarily useful during local development in which each
// message should be delivered into a single mail address for inspection.
if (isset($this->to['address'])) {
$this->setGlobalToAndRemoveCcAndBcc($message);
}
/**
* @event mailer.prepareSend
* Fires before the mailer processes the sending action
*
* Parameters:
* - $view: View code as a string
* - $message: Illuminate\Mail\Message object, check Swift_Mime_SimpleMessage for useful functions.
*
* Example usage (stops the sending process):
*
* Event::listen('mailer.prepareSend', function ((\October\Rain\Mail\Mailer) $mailerInstance, (string) $view, (\Illuminate\Mail\Message) $message) {
* return false;
* });
*
* Or
*
* $mailerInstance->bindEvent('mailer.prepareSend', function ((string) $view, (\Illuminate\Mail\Message) $message) {
* return false;
* });
*
*/
if (
($this->fireEvent('mailer.prepareSend', [$view, $message], true) === false) ||
(Event::fire('mailer.prepareSend', [$this, $view, $message], true) === false)
) {
return;
}
// Next we will determine if the message should be sent. We give the developer
// one final chance to stop this message and then we will send it to all of
// its recipients. We will then fire the sent event for the sent message.
$symfonyMessage = $message->getSymfonyMessage();
if ($this->shouldSendMessage($symfonyMessage, $data)) {
$symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);
if ($symfonySentMessage) {
$sentMessage = new SentMessage($symfonySentMessage);
$this->dispatchSentEvent($sentMessage, $data);
/**
* @event mailer.send
* Fires after the message has been sent
*
* Example usage (logs the message):
*
* Event::listen('mailer.send', function ((\October\Rain\Mail\Mailer) $mailerInstance, (string) $view, (\Illuminate\Mail\Message) $message) {
* \Log::info("Message was rendered with $view and sent");
* });
*
* Or
*
* $mailerInstance->bindEvent('mailer.send', function ((string) $view, (\Illuminate\Mail\Message) $message) {
* \Log::info("Message was rendered with $view and sent");
* });
*
*/
$this->fireEvent('mailer.send', [$view, $message]);
Event::fire('mailer.send', [$this, $view, $message]);
return $sentMessage;
}
}
}
/**
* sendTo is a helper for send() method, the first argument can take a single email or an
* array of recipients where the key is the address and the value is the name.
*
* @param array $recipients
* @param string|array $view
* @param array $data
* @param mixed $callback
* @param array $options
* @return void
*/
public function sendTo($recipients, $view, array $data = [], $callback = null, $options = [])
{
if ($callback && !$options && !is_callable($callback)) {
$options = $callback;
}
if (is_bool($options)) {
$queue = $options;
$bcc = false;
}
else {
extract(array_merge([
'queue' => false,
'bcc' => false
], $options));
}
$method = $queue === true ? 'queue' : 'send';
$recipients = $this->processRecipients($recipients);
return $this->{$method}($view, $data, function ($message) use ($recipients, $callback, $bcc) {
$method = $bcc === true ? 'bcc' : 'to';
foreach ($recipients as $address => $name) {
$message->{$method}($address, $name);
}
if (is_callable($callback)) {
$callback($message);
}
});
}
/**
* queue a new e-mail message for sending.
*
* @param string|array $view
* @param array $data
* @param \Closure|string $callback
* @param string|null $queue
* @return mixed
*/
public function queue($view, $data = null, $callback = null, $queue = null)
{
if (!$view instanceof MailableContract) {
$mailable = $this->buildQueueMailable($view, $data, $callback, $queue);
$queue = null;
}
else {
$mailable = $view;
$queue = $queue ?? $data;
}
return parent::queue($mailable, $queue);
}
/**
* queueOn queues a new e-mail message for sending on the given queue.
*
* @param string $queue
* @param string|array $view
* @param array $data
* @param \Closure|string $callback
* @return mixed
*/
public function queueOn($queue, $view, $data = null, $callback = null)
{
return $this->queue($view, $data, $callback, $queue);
}
/**
* later queues a new e-mail message for sending after (n) seconds.
*
* @param int $delay
* @param string|array $view
* @param array $data
* @param \Closure|string $callback
* @param string|null $queue
* @return mixed
*/
public function later($delay, $view, $data = null, $callback = null, $queue = null)
{
if (!$view instanceof MailableContract) {
$mailable = $this->buildQueueMailable($view, $data, $callback, $queue);
$queue = null;
}
else {
$mailable = $view;
$queue = $queue ?? $data;
}
return parent::later($delay, $mailable, $queue);
}
/**
* laterOn queues a new e-mail message for sending after (n) seconds on the given queue.
*
* @param string $queue
* @param int $delay
* @param string|array $view
* @param array $data
* @param \Closure|string $callback
* @return mixed
*/
public function laterOn($queue, $delay, $view, ?array $data = null, $callback = null)
{
return $this->later($delay, $view, $data, $callback, $queue);
}
/**
* buildQueueMailable for a queued email job.
*
* @param mixed $callback
* @return mixed
*/
protected function buildQueueMailable($view, $data, $callback, $queue)
{
$mailable = new Mailable;
$mailable->locale(App::getLocale());
$mailable->siteContext(Site::getSiteIdFromContext());
$mailable->view($view)->withSerializedData($data);
if ($queue !== null) {
$mailable->onQueue($queue);
}
if ($callback !== null) {
call_user_func($callback, $mailable);
}
/**
* @event mailer.buildQueueMailable
* Process the mailable object used when adding mail to the queue
*
* Example usage:
*
* Event::listen('mailer.buildQueueMailable', function ((\October\Rain\Mail\Mailer) $mailerInstance, (\October\Rain\Mail\Mailable) $mailable) {
* $mailable->mailer('smtp');
* });
*
*/
$this->fireEvent('mailer.buildQueueMailable', [$mailable]);
Event::fire('mailer.buildQueueMailable', [$this, $mailable]);
return $mailable;
}
/**
* raw sends a new message when only a raw text part.
*
* @param string $text
* @param mixed $callback
* @return int
*/
public function raw($view, $callback)
{
if (!is_array($view)) {
$view = ['raw' => $view];
}
elseif (!array_key_exists('raw', $view)) {
$view['raw'] = true;
}
return $this->send($view, [], $callback);
}
/**
* rawTo helper for raw() method, send a new message when only a raw text part.
*
* @param array $recipients
* @param string $view
* @param mixed $callback
* @param array $options
* @return int
*/
public function rawTo($recipients, $view, $callback = null, $options = [])
{
if (!is_array($view)) {
$view = ['raw' => $view];
}
elseif (!array_key_exists('raw', $view)) {
$view['raw'] = true;
}
return $this->sendTo($recipients, $view, [], $callback, $options);
}
/**
* processRecipients object, which can look like the following:
* - (string) admin@domain.tld
* - (object) ['email' => 'admin@domain.tld', 'name' => 'Adam Person']
* - (array) ['admin@domain.tld' => 'Adam Person', ...]
* - (array) [ (object|array) ['email' => 'admin@domain.tld', 'name' => 'Adam Person'], [...] ]
* @param mixed $recipients
* @return array
*/
protected function processRecipients($recipients)
{
$result = [];
if (is_string($recipients)) {
$result[$recipients] = null;
}
elseif (is_array($recipients) || $recipients instanceof Collection) {
foreach ($recipients as $address => $person) {
if (is_string($person)) {
$result[$address] = $person;
}
elseif (is_object($person)) {
if (empty($person->email) && empty($person->address)) {
continue;
}
$address = !empty($person->email) ? $person->email : $person->address;
$name = !empty($person->name) ? $person->name : null;
$result[$address] = $name;
}
elseif (is_array($person)) {
if (!$address = Arr::get($person, 'email', Arr::get($person, 'address'))) {
continue;
}
$result[$address] = Arr::get($person, 'name');
}
}
}
elseif (is_object($recipients)) {
if (!empty($recipients->email) || !empty($recipients->address)) {
$address = !empty($recipients->email) ? $recipients->email : $recipients->address;
$name = !empty($recipients->name) ? $recipients->name : null;
$result[$address] = $name;
}
}
return $result;
}
/**
* addContent to a given message.
*
* @param \Illuminate\Mail\Message $message
* @param string $view
* @param string $plain
* @param string $raw
* @param array $data
* @return void
*/
protected function addContent($message, $view, $plain, $raw, $data)
{
/**
* @event mailer.beforeAddContent
* Fires before the mailer adds content to the message
*
* Example usage (stops the content adding process):
*
* Event::listen('mailer.beforeAddContent', function ((\October\Rain\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (string) $plain, (string) $raw, (array) $data) {
* return false;
* });
*
* Or
*
* $mailerInstance->bindEvent('mailer.beforeAddContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (string) $plain, (string) $raw, (array) $data) {
* return false;
* });
*
*/
if (
($this->fireEvent('mailer.beforeAddContent', [$message, $view, $data, $raw, $plain], true) === false) ||
(Event::fire('mailer.beforeAddContent', [$this, $message, $view, $data, $raw, $plain], true) === false)
) {
return;
}
$html = null;
$text = null;
if (isset($view)) {
$viewContent = $this->renderView($view, $data);
$result = MailParser::parse($viewContent);
$html = $result['html'];
if ($result['text']) {
$text = $result['text'];
}
// Subject
$customSubject = $message->getSymfonyMessage()->getSubject();
if (
empty($customSubject) &&
($subject = Arr::get($result['settings'], 'subject'))
) {
$message->subject($subject);
}
}
if (isset($plain)) {
$text = $this->renderView($plain, $data);
}
if (isset($raw)) {
$text = $raw;
}
$this->addContentRaw($message, $html, $text);
/**
* @event mailer.addContent
* Fires after the mailer has added content to the message
*
* Example usage (Logs that content has been added):
*
* Event::listen('mailer.addContent', function ((\October\Rain\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (array) $data) {
* \Log::info("$view has had content added to the message");
* });
*
* Or
*
* $mailerInstance->bindEvent('mailer.addContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (array) $data) {
* \Log::info("$view has had content added to the message");
* });
*
*/
$this->fireEvent('mailer.addContent', [$message, $view, $data]);
Event::fire('mailer.addContent', [$this, $message, $view, $data]);
}
/**
* addContentRaw to a given message.
*
* @param \Illuminate\Mail\Message $message
* @param string $html
* @param string $text
* @return void
*/
protected function addContentRaw($message, $html, $text)
{
if (isset($html)) {
$message->html($html);
}
if (isset($text)) {
$message->text($text);
}
}
/**
* pretend tells the mailer to not really send messages.
*
* @param bool $value
* @return void
*/
public function pretend($value = true)
{
if ($value) {
$this->pretendingOriginal = Config::get('mail.driver');
Config::set('mail.driver', 'log');
}
else {
Config::set('mail.driver', $this->pretendingOriginal);
}
}
}
================================================
FILE: src/Network/Http.php
================================================
...
* echo $result->code; // Outputs: 200
* echo $result->headers['Content-Type']; // Outputs: text/html; charset=UTF-8
*
* Http::post('http://octobercms.com', function($http){
*
* // Sets a HTTP header
* $http->header('Rest-Key', '...');
*
* // Set a proxy of type (http, socks4, socks5)
* $http->proxy('type', 'host', 'port', 'username', 'password');
*
* // Use basic authentication
* $http->auth('user', 'pass');
*
* // Sends data with the request
* $http->data('foo', 'bar');
* $http->data(['key' => 'value', ...]);
*
* // Disable redirects
* $http->noRedirect();
*
* // Check host SSL certificate
* $http->verifySSL();
*
* // Sets the timeout duration
* $http->timeout(3600);
*
* // Write response to a file
* $http->toFile('some/path/to/a/file.txt');
*
* // Sets a cURL option manually
* $http->setOption(CURLOPT_SSL_VERIFYHOST, false);
*
* });
*
*/
class Http
{
const METHOD_GET = 'GET';
const METHOD_POST = 'POST';
const METHOD_DELETE = 'DELETE';
const METHOD_PATCH = 'PATCH';
const METHOD_PUT = 'PUT';
const METHOD_OPTIONS = 'OPTIONS';
/**
* @var string url is the HTTP address to use
*/
public $url;
/**
* @var string method the request should use
*/
public $method;
/**
* @var array headers to be sent with the request
*/
public $headers = [];
/**
* @var callable headerCallbackFunc is a custom function for handling response headers
*/
public $headerCallbackFunc;
/**
* @var string body is the last response body
*/
public $body = '';
/**
* @var string rawBody is the last response body (without headers extracted)
*/
public $rawBody = '';
/**
* @var array code is the last returned HTTP code
*/
public $code;
/**
* @var array info is the cURL response information
*/
public $info;
/**
* @var array requestOptions contains cURL Options
*/
public $requestOptions;
/**
* @var array requestData
*/
public $requestData;
/**
* @var array requestHeaders
*/
public $requestHeaders;
/**
* @var string argumentSeparator
*/
public $argumentSeparator = '&';
/**
* @var string streamFile is the file to use when writing to a file
*/
public $streamFile;
/**
* @var string streamFilter is the filter to apply when writing response to a file
*/
public $streamFilter;
/**
* @var int maxRedirects allowed
*/
public $maxRedirects = 10;
/**
* @var int redirectCount is an internal counter
*/
protected $redirectCount = null;
/**
* @var bool hasFileData determines if files are being sent with the request
*/
protected $hasFileData = false;
/**
* make the object with common properties
* @param string $url HTTP request address
* @param string $method Request method (GET, POST, PUT, DELETE, etc)
* @param callable $options Callable helper function to modify the object
*/
public static function make($url, $method, $options = null): Http
{
$http = new self;
$http->url = $url;
$http->method = $method;
if ($options && is_callable($options)) {
$options($http);
}
return $http;
}
/**
* get makes a HTTP GET call
* @param string $url
* @param array $options
* @return self
*/
public static function get($url, $options = null): Http
{
$http = self::make($url, self::METHOD_GET, $options);
return $http->send();
}
/**
* post makes a HTTP POST call
* @param string $url
* @param array $options
* @return self
*/
public static function post($url, $options = null): Http
{
$http = self::make($url, self::METHOD_POST, $options);
return $http->send();
}
/**
* delete makes a HTTP DELETE call
* @param string $url
* @param array $options
* @return self
*/
public static function delete($url, $options = null): Http
{
$http = self::make($url, self::METHOD_DELETE, $options);
return $http->send();
}
/**
* patch makes a HTTP PATCH call
* @param string $url
* @param array $options
* @return self
*/
public static function patch($url, $options = null): Http
{
$http = self::make($url, self::METHOD_PATCH, $options);
return $http->send();
}
/**
* put makes a HTTP PUT call
* @param string $url
* @param array $options
*/
public static function put($url, $options = null): Http
{
$http = self::make($url, self::METHOD_PUT, $options);
return $http->send();
}
/**
* options makes a HTTP OPTIONS call
* @param string $url
* @param array $options
*/
public static function options($url, $options = null): Http
{
$http = self::make($url, self::METHOD_OPTIONS, $options);
return $http->send();
}
/**
* send the HTTP request
*/
public function send(): Http
{
if (!function_exists('curl_init')) {
echo 'cURL PHP extension required.'.PHP_EOL;
exit(1);
}
/*
* Create and execute the cURL Resource
*/
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $this->url);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
if (defined('CURLOPT_FOLLOWLOCATION') && !ini_get('open_basedir')) {
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_MAXREDIRS, $this->maxRedirects);
}
if ($this->requestOptions && is_array($this->requestOptions)) {
curl_setopt_array($curl, $this->requestOptions);
}
/*
* Set request method
*/
if ($this->method === self::METHOD_POST) {
curl_setopt($curl, CURLOPT_POST, true);
}
elseif ($this->method !== self::METHOD_GET) {
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->method);
}
/*
* Set request data
*/
if ($this->requestData) {
if (in_array($this->method, [self::METHOD_POST, self::METHOD_PATCH, self::METHOD_PUT])) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $this->getRequestData());
}
elseif (in_array($this->method, [self::METHOD_GET, self::METHOD_DELETE])) {
curl_setopt($curl, CURLOPT_URL, $this->url . '?' . $this->getRequestData());
}
}
/*
* Set request headers
*/
if ($this->requestHeaders) {
$requestHeaders = [];
foreach ($this->requestHeaders as $key => $value) {
$requestHeaders[] = $key . ': ' . $value;
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $requestHeaders);
}
/*
* Custom header function
*/
if ($this->headerCallbackFunc) {
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_HEADERFUNCTION, $this->headerCallbackFunc);
}
/*
* Execute output to file
*/
if ($this->streamFile) {
$stream = fopen($this->streamFile, 'w');
if ($this->streamFilter) {
stream_filter_append($stream, $this->streamFilter, STREAM_FILTER_WRITE);
}
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_FILE, $stream);
curl_exec($curl);
}
/*
* Execute output to variable
*/
else {
$response = $this->rawBody = curl_exec($curl);
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$this->headers = $this->headerToArray(substr($response, 0, $headerSize));
$this->body = substr($response, $headerSize);
}
$this->info = curl_getinfo($curl);
$this->code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
/*
* Close resources
*/
curl_close($curl);
if ($this->streamFile) {
fclose($stream);
}
/*
* Emulate FOLLOW LOCATION behavior
*/
if (!defined('CURLOPT_FOLLOWLOCATION') || ini_get('open_basedir')) {
if ($this->redirectCount === null) {
$this->redirectCount = $this->maxRedirects;
}
if (in_array($this->code, [301, 302])) {
$this->url = Arr::get($this->info, 'redirect_url');
if (!empty($this->url) && $this->redirectCount > 0) {
$this->redirectCount -= 1;
return $this->send();
}
}
}
return $this;
}
/**
* getRequestData returns the request data set
*/
public function getRequestData()
{
if (empty($this->requestData)) {
return isset($this->requestOptions[CURLOPT_POSTFIELDS])
? $this->requestOptions[CURLOPT_POSTFIELDS]
: '';
}
if ($this->method === self::METHOD_GET || !$this->hasFileData) {
return http_build_query($this->requestData, '', $this->argumentSeparator);
}
// This will trigger multipart/form-data content type and needs an array,
// make some attempt at supporting multidimensional array values
if (is_array($this->requestData)) {
$out = [];
foreach ($this->requestData as $var => $dat) {
$out[$var] = is_array($dat)
? http_build_query($dat, '', $this->argumentSeparator)
: $dat;
}
return $out;
}
return $this->requestData;
}
/**
* data added to the request
*/
public function data($key, $value = null): Http
{
if (is_array($key)) {
foreach ($key as $_key => $_value) {
$this->data($_key, $_value);
}
return $this;
}
$this->requestData[$key] = $value;
return $this;
}
/**
* dataFile added to the request
*/
public function dataFile(string $key, string $filePath): Http
{
$this->hasFileData = true;
return $this->data($key, curl_file_create($filePath));
}
/**
* header added to the request
* @param string $value
*/
public function header($key, $value = null): Http
{
if (is_array($key)) {
foreach ($key as $_key => $_value) {
$this->header($_key, $_value);
}
return $this;
}
$this->requestHeaders[$key] = $value;
return $this;
}
/**
* proxy to use with this request
*/
public function proxy($type, $host, $port, $username = null, $password = null): Http
{
if ($type === 'http') {
$this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
}
elseif ($type === 'socks4') {
$this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4);
}
elseif ($type === 'socks5') {
$this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
}
$this->setOption(CURLOPT_PROXY, $host . ':' . $port);
if ($username && $password) {
$this->setOption(CURLOPT_PROXYUSERPWD, $username . ':' . $password);
}
return $this;
}
/**
* auth adds authentication to the request
* @param string $user
* @param string $pass
*/
public function auth($user, $pass = null): Http
{
if (strpos($user, ':') !== false && !$pass) {
list($user, $pass) = explode(':', $user);
}
$this->setOption(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
$this->setOption(CURLOPT_USERPWD, $user . ':' . $pass);
return $this;
}
/**
* noRedirect disables follow location (redirects)
*/
public function noRedirect(): Http
{
if (defined('CURLOPT_FOLLOWLOCATION') && !ini_get('open_basedir')) {
$this->setOption(CURLOPT_FOLLOWLOCATION, false);
}
else {
$this->maxRedirects = 0;
}
return $this;
}
/**
* verifySSL enabled for the request
*/
public function verifySSL(): Http
{
$this->setOption(CURLOPT_SSL_VERIFYPEER, true);
$this->setOption(CURLOPT_SSL_VERIFYHOST, true);
return $this;
}
/**
* timeout for the request
* @param string $timeout
*/
public function timeout($timeout): Http
{
$this->setOption(CURLOPT_CONNECTTIMEOUT, $timeout);
$this->setOption(CURLOPT_TIMEOUT, $timeout);
return $this;
}
/**
* toFile write the response to a file
* @param string $path Path to file
* @param string $filter Stream filter as listed in stream_get_filters()
*/
public function toFile($path, $filter = null): Http
{
$this->streamFile = $path;
if ($filter) {
$this->streamFilter = $filter;
}
return $this;
}
/**
* headerCallback sets a custom method for handling headers
*
* function header_callback($curl, string $headerLine) {}
*
*/
public function headerCallback($callback): Http
{
$this->headerCallbackFunc = $callback;
return $this;
}
/**
* setOption as a single option to the request
* @param string $option
* @param string $value
*/
public function setOption($option, $value = null): Http
{
if (is_array($option)) {
foreach ($option as $_option => $_value) {
$this->setOption($_option, $_value);
}
return $this;
}
if (is_string($option) && defined($option)) {
$optionKey = constant($option);
$this->requestOptions[$optionKey] = $value;
}
elseif (is_int($option)) {
$constants = get_defined_constants(true);
$curlOptConstants = array_flip(array_filter($constants['curl'], function ($key) {
return strpos($key, 'CURLOPT_') === 0;
}, ARRAY_FILTER_USE_KEY));
if (isset($curlOptConstants[$option])) {
$this->requestOptions[$option] = $value;
}
else {
throw new ApplicationException('$option parameter must be a CURLOPT constant or equivalent integer');
}
}
else {
throw new ApplicationException('$option parameter must be a CURLOPT constant or equivalent integer');
}
return $this;
}
/**
* Handy if this object is called directly.
* @return string The last response.
*/
public function __toString()
{
return (string) $this->body;
}
/**
* headerToArray turns a header string into an array
*/
protected function headerToArray(string $header): array
{
$headers = [];
$parts = explode("\r\n", $header);
foreach ($parts as $singleHeader) {
$delimiter = strpos($singleHeader, ': ');
if ($delimiter !== false) {
$key = substr($singleHeader, 0, $delimiter);
$val = substr($singleHeader, $delimiter + 2);
$headers[$key] = $val;
}
else {
$delimiter = strpos($singleHeader, ' ');
if ($delimiter !== false) {
$key = substr($singleHeader, 0, $delimiter);
$val = substr($singleHeader, $delimiter + 1);
$headers[$key] = $val;
}
}
}
return $headers;
}
}
================================================
FILE: src/Parse/Bracket.php
================================================
false,
'newlineToBr' => false,
'filters' => []
];
public function __construct($options = [])
{
$this->setOptions($options);
}
public function setOptions($options = [])
{
$this->options = array_merge($this->options, $options);
}
/**
* Static helper for new instances of this class.
* @param string $template
* @param array $vars
* @param array $options
* @return self
*/
public static function parse($template, $vars = [], $options = [])
{
$obj = new static($options);
return $obj->parseString($template, $vars);
}
/**
* Parse a string against data
* @param string $string
* @param array $data
* @return string
*/
public function parseString($string, $data)
{
if (!is_string($string) || !strlen(trim($string))) {
return false;
}
foreach ($data as $key => $value) {
if (is_array($value)) {
$string = $this->parseLoop($key, $value, $string);
}
else {
$string = $this->parseKey($key, $value, $string);
$string = $this->parseKeyFilters($key, $value, $string);
$string = $this->parseKeyBooleans($key, $value, $string);
}
}
return $string;
}
/**
* Process a single key
* @param string $key
* @param string $value
* @param string $string
* @return string
*/
protected function parseKey($key, $value, $string)
{
if (isset($this->options['encodeHtml']) && $this->options['encodeHtml']) {
$value = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}
if (isset($this->options['newlineToBr']) && $this->options['newlineToBr']) {
$value = nl2br($value);
}
$returnStr = str_replace(static::CHAR_OPEN.$key.static::CHAR_CLOSE, $value, $string);
return $returnStr;
}
/**
* Look for filtered variables and replace them
* @param string $key
* @param string $value
* @param string $string
* @return string
*/
protected function parseKeyFilters($key, $value, $string)
{
if (!$filters = $this->options['filters']) {
return $string;
}
$returnStr = $string;
foreach ($filters as $filter => $func) {
$charKey = static::CHAR_OPEN.$key.'|'.$filter.static::CHAR_CLOSE;
if (is_callable($func) && strpos($string, $charKey) !== false) {
$returnStr = str_replace($charKey, $func($value), $returnStr);
}
}
return $returnStr;
}
/**
* This is an internally used method, the syntax is experimental and may change.
*/
protected function parseKeyBooleans($key, $value, $string)
{
$openKey = static::CHAR_OPEN.'?'.$key.static::CHAR_CLOSE;
$closeKey = static::CHAR_OPEN.'/'.$key.static::CHAR_CLOSE;
if ($value) {
$returnStr = str_replace([$openKey, $closeKey], '', $string);
}
else {
$open = preg_quote($openKey);
$close = preg_quote($closeKey);
$returnStr = preg_replace('|'.$open.'[\s\S]+?'.$close.'|s', '', $string);
}
return $returnStr;
}
/**
* Search for open/close keys and process them in a nested fashion
* @param string $key
* @param array $data
* @param string $string
* @return string
*/
protected function parseLoop($key, $data, $string)
{
$returnStr = '';
$match = $this->parseLoopRegex($string, $key);
if (!$match) {
return $string;
}
foreach ($data as $row) {
$matchedText = $match[1];
foreach ($row as $key => $value) {
if (is_array($value)) {
$matchedText = $this->parseLoop($key, $value, $matchedText);
}
else {
$matchedText = $this->parseKey($key, $value, $matchedText);
$matchedText = $this->parseKeyFilters($key, $value, $matchedText);
$matchedText = $this->parseKeyBooleans($key, $value, $matchedText);
}
}
$returnStr .= $matchedText;
}
return str_replace($match[0], $returnStr, $string);
}
/**
* Internal method, returns a Regular expression for parsing
* a looping tag.
* @param string $string
* @param string $key
* @return string
*/
protected function parseLoopRegex($string, $key)
{
$open = preg_quote(static::CHAR_OPEN);
$close = preg_quote(static::CHAR_CLOSE);
$regex = '|';
$regex .= $open.$key.$close; // Open
$regex .= '(.+?)'; // Content
$regex .= $open.'/'.$key.$close; // Close
$regex .='|s';
preg_match($regex, $string, $match);
return $match ?: false;
}
}
================================================
FILE: src/Parse/ComponentParser.php
================================================
parseContentInternal($contents);
}
/**
* parseContentInternal handler
*/
protected function parseContentInternal($content)
{
// Optimized regular expression to match self-closing and nested custom HTML elements
$pattern = '/]*)?\/>|]*)?>(.*?)<\/x-\3>/s';
// Find all matches
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
// Self-closing tag
if (!empty($match[1])) {
$tag = $match[1];
$attributes = $this->parseAttributes($match[2]);
$replacement = $this->parseContent($tag, $attributes);
$content = str_replace($match[0], $replacement, $content);
}
// Nested tag
elseif (!empty($match[3])) {
$tag = $match[3];
$attributes = $this->parseAttributes($match[4]);
$attributes['slot'] = $this->parseContentInternal(trim($match[5]));
$replacement = $this->parseContent($tag, $attributes);
$content = str_replace($match[0], $replacement, $content);
}
}
return $content;
}
/**
* parseAttributes extracts attributes from the component tag
*/
protected function parseAttributes($attributeString)
{
$attributes = [];
if (preg_match_all('/(\w+)="([^"]*)"/', $attributeString, $attrMatches, PREG_SET_ORDER)) {
foreach ($attrMatches as $attr) {
$attributes[$attr[1]] = $attr[2];
}
}
return $attributes;
}
/**
* parseContent uses a global registry of components
*/
protected function parseContent($tag, $attributes)
{
// ...@todo...
return "";
}
}
================================================
FILE: src/Parse/Ini.php
================================================
parsePreProcess($contents);
$contents = parse_ini_string($contents, true);
$contents = $this->parsePostProcess($contents);
return $contents;
}
/**
* parseFile supplied INI file contents in to a PHP array.
* @param string $fileName File to read contents and parse.
* @return array
*/
public function parseFile($fileName)
{
$contents = file_get_contents($fileName);
return $this->parse($contents);
}
/**
* render formats an INI formatted string from an array of data variables
*
* Supported options:
* - exceptionOnInvalidKey: if an exception must be thrown on invalid key names.
*
* @param array $vars
* @param array $options
* @return string
*/
public function render($vars = [], $options = [])
{
extract(array_merge([
'exceptionOnInvalidKey' => false,
], $options));
$content = '';
$sections = [];
foreach ($vars as $key => $value) {
if ($this->validateKeyName($key) !== true) {
if ($exceptionOnInvalidKey) {
throw new Exception("Key name [$key] is invalid for INI syntax");
}
continue;
}
if (is_array($value)) {
if ($this->isFinalArray($value)) {
foreach ($value as $_value) {
$content .= $key.'[] = '.$this->evalValue($_value).PHP_EOL;
}
}
else {
$sections[$key] = $this->renderProperties($value);
}
}
elseif (strlen($value)) {
$content .= $key.' = '.$this->evalValue($value).PHP_EOL;
}
}
foreach ($sections as $key => $section) {
$content .= PHP_EOL.'['.$key.']'.PHP_EOL.$section;
}
return trim($content);
}
//
// Parse
//
/**
* parsePreProcess converts key names traditionally invalid, "][", and
* replaces them with a valid character "|" so parse_ini_string
* can function correctly. It also forces arrays to have unique
* indexes so their integrity is maintained.
* @param string $contents INI contents to parse.
* @return string
*/
protected function parsePreProcess($contents)
{
// Sanitize environment variable interpolation syntax to prevent
// parse_ini_string from resolving ${VAR} to environment values
$contents = str_replace('${', 'oc_env_open{', $contents);
// Normalize EOL
$contents = preg_replace('~\R~u', PHP_EOL, $contents);
$contents = explode(PHP_EOL, $contents);
$count = 0;
$lastName = null;
foreach ($contents as $key => $content) {
if (strpos($content, '=') === false) {
continue;
}
$parts = explode('=', $content, 2);
if (count($parts) < 2) {
continue;
}
$varName = $parts[0];
if ($lastName !== $varName) {
$count = 0;
$lastName = null;
}
if (
($lastName === null || $lastName === $varName) &&
strpos($varName, '[]') !== false
) {
$varName = str_replace('[]', '['.$count.']', $varName);
$count++;
}
$lastName = $parts[0];
$parts[0] = str_replace('][', '|', $varName);
$contents[$key] = implode('=', $parts);
}
return implode(PHP_EOL, $contents);
}
/**
* parsePostProcess takes the valid key name from pre processing and
* converts it back to a real PHP array. Eg:
* - name[validation|regex|message]
* Converts to:
* - name => [validation => [regex => [message]]]
* @param array $array
* @return array
*/
protected function parsePostProcess($array)
{
$result = [];
foreach ($array as $key => &$value) {
if (is_string($value)) {
$value = str_replace('oc_env_open{', '${', $value);
}
$this->expandProperty($result, $key, $value);
if (is_array($value)) {
$result[$key] = $this->parsePostProcess($value);
}
}
return $result;
}
/**
* expandProperty expands a single array property from traditional INI syntax.
* If no key is given to the method, the entire array will be replaced.
* @param array $array
* @param string $key
* @param mixed $value
* @return array
*/
protected function expandProperty(&$array, $key, $value)
{
if (is_null($key)) {
return $array = $value;
}
$keys = explode('|', $key);
while (count($keys) > 1) {
$key = array_shift($keys);
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array =& $array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
//
// Render
//
/**
* renderProperties renders section properties.
* @param array $vars
* @return string
*/
protected function renderProperties($vars = [])
{
$content = '';
foreach ($vars as $key => $value) {
if (is_array($value)) {
if ($this->isFinalArray($value)) {
foreach ($value as $_value) {
$content .= $key.'[] = '.$this->evalValue($_value).PHP_EOL;
}
}
else {
$value = $this->flattenProperties($value);
foreach ($value as $_key => $_value) {
if (is_array($_value)) {
foreach ($_value as $__value) {
$content .= $key.'['.$_key.'][] = '.$this->evalValue($__value).PHP_EOL;
}
}
else {
$content .= $key.'['.$_key.'] = '.$this->evalValue($_value).PHP_EOL;
}
}
}
}
elseif (strlen($value)) {
$content .= $key.' = '.$this->evalValue($value).PHP_EOL;
}
}
return $content;
}
/**
* flattenProperties flattens a multi-dimensional associative array for traditional INI syntax.
* @param array $array
* @param string $prepend
* @return array
*/
protected function flattenProperties($array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
if (is_array($value)) {
if ($this->isFinalArray($value)) {
$results[$prepend.$key] = $value;
}
else {
$results = array_merge($results, $this->flattenProperties($value, $prepend.$key.']['));
}
}
else {
$results[$prepend.$key] = $value;
}
}
return $results;
}
/**
* evalValue converts a PHP value to make it suitable for INI format.
* Strings are escaped.
* @param string $value Specifies the value to process
* @return string Returns the processed value
*/
protected function evalValue($value)
{
// Numeric
if (is_numeric($value)) {
return $value;
}
// String (default)
$value = str_replace('"', '\"', $value);
$value = preg_replace('~\\\"([\r\n])~', '\\\"""$1', $value);
return '"'.$value.'"';
}
/**
* isFinalArray checks if the array is the final node in a multidimensional array.
* Checked supplied array is not associative and contains no array values.
* @param array $array
* @return bool
*/
protected function isFinalArray(array $array)
{
return !empty($array) &&
!count(array_filter($array, 'is_array')) &&
!count(array_filter(array_keys($array), 'is_string'));
}
/**
* validateKeyName returns false if an invalid key name is found
*/
protected function validateKeyName($keyName): bool
{
$invalidChars = '?{}|&~!()^"#;=';
foreach (str_split($invalidChars) as $char) {
if (strpos($keyName, $char) !== false) {
return false;
}
}
return true;
}
}
================================================
FILE: src/Parse/Markdown.php
================================================
parseInternal($text);
}
/**
* parseClean enables safe mode where the resulting HTML is cleaned
* using a sanitizer
*/
public function parseClean($text): string
{
$result = Html::clean($this->parse($text));
$this->parser = null;
return $result;
}
/**
* parseSafe is stricter than parse clean allowing no HTML at all
* except for basic protocols such as https://, ftps://, mailto:, etc.
*/
public function parseSafe($text): string
{
$this->getParser()->setSafeMode(true);
$result = $this->parse($text);
$this->parser = null;
return $result;
}
/**
* parseIndent disables code blocks caused by indentation
*/
public function parseIndent($text): string
{
$this->getParser()->setIndentMode(false);
$result = $this->parse($text);
$this->parser = null;
return $result;
}
/**
* parseLine parses a single line
*/
public function parseLine($text): string
{
return $this->parseInternal($text, 'line');
}
/**
* parseInternal is an internal method for parsing
*/
protected function parseInternal($text, $method = 'text'): string
{
$data = new MarkdownData($text);
$this->fireEvent('beforeParse', $data, false);
Event::fire('markdown.beforeParse', $data, false);
$result = $data->text;
$result = $this->getParser()->$method($result);
$data->text = $result;
// The markdown.parse gets passed both the original
// input and the result so far.
$this->fireEvent('parse', [$text, $data], false);
Event::fire('markdown.parse', [$text, $data], false);
return $data->text;
}
/**
* getParser returns an instance of the parser
*/
protected function getParser(): ParsedownExtra
{
if ($this->parser === null) {
$this->parser = new ParsedownExtra;
}
return $this->parser;
}
}
================================================
FILE: src/Parse/MarkdownData.php
================================================
text = $text;
}
}
================================================
FILE: src/Parse/ParseServiceProvider.php
================================================
app->singleton('parse.markdown', function ($app) {
return new Markdown;
});
$this->app->singleton('parse.yaml', function ($app) {
return new Yaml;
});
$this->app->singleton('parse.twig', function ($app) {
return new Twig;
});
$this->app->singleton('parse.ini', function ($app) {
return new Ini;
});
}
/**
* provides the returned services.
* @return array
*/
public function provides()
{
return [
'parse.markdown',
'parse.yaml',
'parse.twig',
'parse.ini'
];
}
}
================================================
FILE: src/Parse/Parsedown/Parsedown.php
================================================
['Header'],
'*' => ['Rule', 'List'],
'+' => ['List'],
'-' => ['SetextHeader', 'Table', 'Rule', 'List'],
'0' => ['List'],
'1' => ['List'],
'2' => ['List'],
'3' => ['List'],
'4' => ['List'],
'5' => ['List'],
'6' => ['List'],
'7' => ['List'],
'8' => ['List'],
'9' => ['List'],
':' => ['Table'],
'<' => ['Comment', 'Markup'],
'=' => ['SetextHeader'],
'>' => ['Quote'],
'[' => ['Reference'],
'_' => ['Rule'],
'`' => ['FencedCode'],
'|' => ['Table'],
'~' => ['FencedCode'],
];
/**
* @var array unmarkedBlockTypes
*/
protected $unmarkedBlockTypes = [
'Code',
];
/**
* @var mixed safeLinksWhitelist
*/
protected $safeLinksWhitelist = array(
'http://',
'https://',
'ftp://',
'ftps://',
'mailto:',
'tel:',
'data:image/png;base64,',
'data:image/gif;base64,',
'data:image/jpeg;base64,',
'irc:',
'ircs:',
'git:',
'ssh:',
'news:',
'steam:',
);
/**
* @var array InlineTypes
*/
protected $InlineTypes = array(
'!' => ['Image'],
'&' => ['SpecialCharacter'],
'*' => ['Emphasis'],
':' => ['Url'],
'<' => ['UrlTag', 'EmailTag', 'Markup'],
'[' => ['Link'],
'_' => ['Emphasis'],
'`' => ['Code'],
'~' => ['Strikethrough'],
'\\' => ['EscapeSequence'],
);
/**
* @var string inlineMarkerList
*/
protected $inlineMarkerList = '!*_&[:<`~\\';
/**
* @var array instances
*/
protected static $instances = [];
/**
* @var array DefinitionData
*/
protected $DefinitionData;
/**
* @var array specialCharacters
*/
protected $specialCharacters = [
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
];
/**
* @var array StrongRegex
*/
protected $StrongRegex = [
'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
];
/**
* @var array EmRegex
*/
protected $EmRegex = [
'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
];
/**
* @var array regexHtmlAttribute
*/
protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
/**
* @var array voidElements
*/
protected $voidElements = [
'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
];
/**
* @var array textLevelElements
*/
protected $textLevelElements = [
'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
'i', 'rp', 'del', 'code', 'strike', 'marquee',
'q', 'rt', 'ins', 'font', 'strong',
's', 'tt', 'kbd', 'mark',
'u', 'xm', 'sub', 'nobr',
'sup', 'ruby',
'var', 'span',
'wbr', 'time',
];
/**
* text
*/
public function text($text)
{
$Elements = $this->textElements($text);
// Convert to markup
$markup = $this->elements($Elements);
// Trim line breaks
$markup = trim($markup, "\n");
return $markup;
}
/**
* textElements
*/
protected function textElements($text)
{
// make sure no definitions are set
$this->DefinitionData = [];
// standardize line breaks
$text = str_replace(array("\r\n", "\r"), "\n", $text);
// remove surrounding line breaks
$text = trim($text, "\n");
// split text into lines
$lines = explode("\n", $text);
// iterate through lines to identify blocks
return $this->linesElements($lines);
}
/**
* setBreaksEnabled
*/
public function setBreaksEnabled($breaksEnabled)
{
$this->breaksEnabled = $breaksEnabled;
return $this;
}
/**
* setMarkupEscaped
*/
public function setMarkupEscaped($markupEscaped)
{
$this->markupEscaped = $markupEscaped;
return $this;
}
/**
* setUrlsLinked
*/
public function setUrlsLinked($urlsLinked)
{
$this->urlsLinked = $urlsLinked;
return $this;
}
/**
* setSafeMode
*/
public function setSafeMode($safeMode)
{
$this->safeMode = (bool) $safeMode;
return $this;
}
/**
* setStrictMode
*/
public function setStrictMode($strictMode)
{
$this->strictMode = (bool) $strictMode;
return $this;
}
/**
* setIndentMode
*/
public function setIndentMode($indentMode)
{
$this->unmarkedBlockTypes = $indentMode === false ? ['Markup'] : ['Code'];
return $this;
}
/**
* lines
*/
protected function lines(array $lines)
{
return $this->elements($this->linesElements($lines));
}
/**
* linesElements
*/
protected function linesElements(array $lines)
{
$Elements = [];
$CurrentBlock = null;
foreach ($lines as $line) {
if (chop($line) === '') {
if (isset($CurrentBlock)) {
$CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
? $CurrentBlock['interrupted'] + 1 : 1
);
}
continue;
}
while (($beforeTab = strstr($line, "\t", true)) !== false) {
$shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
$line = $beforeTab
. str_repeat(' ', $shortage)
. substr($line, strlen($beforeTab) + 1)
;
}
$indent = strspn($line, ' ');
$text = $indent > 0 ? substr($line, $indent) : $line;
$Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
if (isset($CurrentBlock['continuable'])) {
$methodName = 'block' . $CurrentBlock['type'] . 'Continue';
$Block = $this->$methodName($Line, $CurrentBlock);
if (isset($Block)) {
$CurrentBlock = $Block;
continue;
}
else {
if ($this->isBlockCompletable($CurrentBlock['type'])) {
$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
$CurrentBlock = $this->$methodName($CurrentBlock);
}
}
}
$marker = $text[0];
$blockTypes = $this->unmarkedBlockTypes;
if (isset($this->BlockTypes[$marker])) {
foreach ($this->BlockTypes[$marker] as $blockType) {
$blockTypes[] = $blockType;
}
}
foreach ($blockTypes as $blockType) {
$Block = $this->{"block$blockType"}($Line, $CurrentBlock);
if (isset($Block)) {
$Block['type'] = $blockType;
if (!isset($Block['identified'])) {
if (isset($CurrentBlock)) {
$Elements[] = $this->extractElement($CurrentBlock);
}
$Block['identified'] = true;
}
if ($this->isBlockContinuable($blockType)) {
$Block['continuable'] = true;
}
$CurrentBlock = $Block;
continue 2;
}
}
if (isset($CurrentBlock) && $CurrentBlock['type'] === 'Paragraph') {
$Block = $this->paragraphContinue($Line, $CurrentBlock);
}
if (isset($Block)) {
$CurrentBlock = $Block;
}
else {
if (isset($CurrentBlock)) {
$Elements[] = $this->extractElement($CurrentBlock);
}
$CurrentBlock = $this->paragraph($Line);
$CurrentBlock['identified'] = true;
}
}
if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type'])) {
$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
$CurrentBlock = $this->$methodName($CurrentBlock);
}
if (isset($CurrentBlock)) {
$Elements[] = $this->extractElement($CurrentBlock);
}
return $Elements;
}
/**
* extractElement
*/
protected function extractElement(array $Component)
{
if (!isset($Component['element'])) {
if (isset($Component['markup'])) {
$Component['element'] = array('rawHtml' => $Component['markup']);
}
elseif (isset($Component['hidden'])) {
$Component['element'] = [];
}
}
return $Component['element'];
}
/**
* isBlockContinuable
*/
protected function isBlockContinuable($Type)
{
return method_exists($this, 'block' . $Type . 'Continue');
}
/**
* isBlockCompletable
*/
protected function isBlockCompletable($Type)
{
return method_exists($this, 'block' . $Type . 'Complete');
}
/**
* blockCode
*/
protected function blockCode($Line, $Block = null)
{
if (isset($Block) && $Block['type'] === 'Paragraph' && ! isset($Block['interrupted'])) {
return;
}
if ($Line['indent'] >= 4) {
$text = substr($Line['body'], 4);
$Block = array(
'element' => array(
'name' => 'pre',
'element' => array(
'name' => 'code',
'text' => $text,
),
),
);
return $Block;
}
}
/**
* blockCodeContinue
*/
protected function blockCodeContinue($Line, $Block)
{
if ($Line['indent'] >= 4) {
if (isset($Block['interrupted'])) {
$Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
unset($Block['interrupted']);
}
$Block['element']['element']['text'] .= "\n";
$text = substr($Line['body'], 4);
$Block['element']['element']['text'] .= $text;
return $Block;
}
}
/**
* blockCodeComplete
*/
protected function blockCodeComplete($Block)
{
return $Block;
}
/**
* blockComment
*/
protected function blockComment($Line)
{
if ($this->markupEscaped || $this->safeMode) {
return;
}
if (strpos($Line['text'], '') !== false) {
$Block['closed'] = true;
}
return $Block;
}
}
/**
* blockCommentContinue
*/
protected function blockCommentContinue($Line, array $Block)
{
if (isset($Block['closed'])) {
return;
}
$Block['element']['rawHtml'] .= "\n" . $Line['body'];
if (strpos($Line['text'], '-->') !== false) {
$Block['closed'] = true;
}
return $Block;
}
/**
* blockFencedCode
*/
protected function blockFencedCode($Line)
{
$marker = $Line['text'][0];
$openerLength = strspn($Line['text'], $marker);
if ($openerLength < 3) {
return;
}
$infostring = trim(substr($Line['text'], $openerLength), "\t ");
if (strpos($infostring, '`') !== false) {
return;
}
$Element = array(
'name' => 'code',
'text' => '',
);
if ($infostring !== '') {
/**
* https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
* Every HTML element may have a class attribute specified.
* The attribute, if specified, must have a value that is a set
* of space-separated tokens representing the various classes
* that the element belongs to.
* [...]
* The space characters, for the purposes of this specification,
* are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
* U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
* U+000D CARRIAGE RETURN (CR).
*/
$language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
$Element['attributes'] = ['class' => "language-$language"];
}
$Block = [
'char' => $marker,
'openerLength' => $openerLength,
'element' => [
'name' => 'pre',
'element' => $Element,
],
];
return $Block;
}
/**
* blockFencedCodeContinue
*/
protected function blockFencedCodeContinue($Line, $Block)
{
if (isset($Block['complete'])) {
return;
}
if (isset($Block['interrupted'])) {
$Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
unset($Block['interrupted']);
}
if (
($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] &&
chop(substr($Line['text'], $len), ' ') === ''
) {
$Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
$Block['complete'] = true;
return $Block;
}
$Block['element']['element']['text'] .= "\n" . $Line['body'];
return $Block;
}
/**
* blockFencedCodeComplete
*/
protected function blockFencedCodeComplete($Block)
{
return $Block;
}
/**
* blockHeader
*/
protected function blockHeader($Line)
{
$level = strspn($Line['text'], '#');
if ($level > 6) {
return;
}
$text = trim($Line['text'], '#');
if ($this->strictMode && isset($text[0]) && $text[0] !== ' ') {
return;
}
$text = trim($text, ' ');
$Block = array(
'element' => array(
'name' => 'h' . $level,
'handler' => array(
'function' => 'lineElements',
'argument' => $text,
'destination' => 'elements',
)
),
);
return $Block;
}
/**
* blockList
*/
protected function blockList($Line, ?array $CurrentBlock = null)
{
list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) {
$contentIndent = strlen($matches[2]);
if ($contentIndent >= 5) {
$contentIndent -= 1;
$matches[1] = substr($matches[1], 0, -$contentIndent);
$matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
}
elseif ($contentIndent === 0) {
$matches[1] .= ' ';
}
$markerWithoutWhitespace = strstr($matches[1], ' ', true);
$Block = array(
'indent' => $Line['indent'],
'pattern' => $pattern,
'data' => array(
'type' => $name,
'marker' => $matches[1],
'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
),
'element' => array(
'name' => $name,
'elements' => [],
),
);
$Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
if ($name === 'ol') {
$listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
if ($listStart !== '1') {
if (
isset($CurrentBlock)
&& $CurrentBlock['type'] === 'Paragraph'
&& ! isset($CurrentBlock['interrupted'])
) {
return;
}
$Block['element']['attributes'] = array('start' => $listStart);
}
}
$Block['li'] = array(
'name' => 'li',
'handler' => array(
'function' => 'li',
'argument' => !empty($matches[3]) ? array($matches[3]) : [],
'destination' => 'elements'
)
);
$Block['element']['elements'][] = & $Block['li'];
return $Block;
}
}
/**
* blockListContinue
*/
protected function blockListContinue($Line, array $Block)
{
if (isset($Block['interrupted']) && empty($Block['li']['handler']['argument'])) {
return null;
}
$requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
if ($Line['indent'] < $requiredIndent && (
(
$Block['data']['type'] === 'ol' &&
preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
) || (
$Block['data']['type'] === 'ul' &&
preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
)
)
) {
if (isset($Block['interrupted'])) {
$Block['li']['handler']['argument'][] = '';
$Block['loose'] = true;
unset($Block['interrupted']);
}
unset($Block['li']);
$text = isset($matches[1]) ? $matches[1] : '';
$Block['indent'] = $Line['indent'];
$Block['li'] = array(
'name' => 'li',
'handler' => array(
'function' => 'li',
'argument' => array($text),
'destination' => 'elements'
)
);
$Block['element']['elements'][] = & $Block['li'];
return $Block;
}
elseif ($Line['indent'] < $requiredIndent && $this->blockList($Line)) {
return null;
}
if ($Line['text'][0] === '[' && $this->blockReference($Line)) {
return $Block;
}
if ($Line['indent'] >= $requiredIndent) {
if (isset($Block['interrupted'])) {
$Block['li']['handler']['argument'][] = '';
$Block['loose'] = true;
unset($Block['interrupted']);
}
$text = substr($Line['body'], $requiredIndent);
$Block['li']['handler']['argument'][] = $text;
return $Block;
}
if (!isset($Block['interrupted'])) {
$text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
$Block['li']['handler']['argument'][] = $text;
return $Block;
}
}
protected function blockListComplete(array $Block)
{
if (isset($Block['loose'])) {
foreach ($Block['element']['elements'] as &$li) {
if (end($li['handler']['argument']) !== '') {
$li['handler']['argument'][] = '';
}
}
}
return $Block;
}
/**
* blockQuote
*/
protected function blockQuote($Line)
{
if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
$Block = array(
'element' => array(
'name' => 'blockquote',
'handler' => array(
'function' => 'linesElements',
'argument' => (array) $matches[1],
'destination' => 'elements',
)
),
);
return $Block;
}
}
/**
* blockQuoteContinue
*/
protected function blockQuoteContinue($Line, array $Block)
{
if (isset($Block['interrupted'])) {
return;
}
if ($Line['text'][0] === '>' && preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
$Block['element']['handler']['argument'][] = $matches[1];
return $Block;
}
if (!isset($Block['interrupted'])) {
$Block['element']['handler']['argument'][] = $Line['text'];
return $Block;
}
}
/**
* blockRule
*/
protected function blockRule($Line)
{
$marker = $Line['text'][0];
if (substr_count($Line['text'], $marker) >= 3 && chop($Line['text'], " $marker") === '') {
$Block = [
'element' => [
'name' => 'hr',
],
];
return $Block;
}
}
/**
* blockSetextHeader
*/
protected function blockSetextHeader($Line, ?array $Block = null)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) {
return;
}
if ($Line['indent'] < 4 && chop(chop($Line['text'], ' '), $Line['text'][0]) === '') {
$Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
return $Block;
}
}
/**
* blockMarkup
*/
protected function blockMarkup($Line)
{
if ($this->markupEscaped || $this->safeMode) {
return;
}
if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) {
$element = strtolower($matches[1]);
if (in_array($element, $this->textLevelElements)) {
return;
}
$Block = array(
'name' => $matches[1],
'element' => array(
'rawHtml' => $Line['text'],
'autobreak' => true,
),
);
return $Block;
}
}
/**
* blockMarkupContinue
*/
protected function blockMarkupContinue($Line, array $Block)
{
if (isset($Block['closed']) || isset($Block['interrupted'])) {
return;
}
$Block['element']['rawHtml'] .= "\n" . $Line['body'];
return $Block;
}
/**
* blockReference
*/
protected function blockReference($Line)
{
if (strpos($Line['text'], ']') !== false
&& preg_match('/^\[(.+?)\]:[ ]*+(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
) {
$id = strtolower($matches[1]);
$Data = [
'url' => $matches[2],
'title' => isset($matches[3]) ? $matches[3] : null,
];
$this->DefinitionData['Reference'][$id] = $Data;
$Block = [
'element' => [],
];
return $Block;
}
}
/**
* blockTable
*/
protected function blockTable($Line, ?array $Block = null)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) {
return;
}
if (
strpos($Block['element']['handler']['argument'], '|') === false &&
strpos($Line['text'], '|') === false &&
strpos($Line['text'], ':') === false ||
strpos($Block['element']['handler']['argument'], "\n") !== false
) {
return;
}
if (chop($Line['text'], ' -:|') !== '') {
return;
}
$alignments = [];
$divider = $Line['text'];
$divider = trim($divider);
$divider = trim($divider, '|');
$dividerCells = explode('|', $divider);
foreach ($dividerCells as $dividerCell) {
$dividerCell = trim($dividerCell);
if ($dividerCell === '') {
return;
}
$alignment = null;
if ($dividerCell[0] === ':') {
$alignment = 'left';
}
if (substr($dividerCell, - 1) === ':') {
$alignment = $alignment === 'left' ? 'center' : 'right';
}
$alignments[] = $alignment;
}
$HeaderElements = [];
$header = $Block['element']['handler']['argument'];
$header = trim($header);
$header = trim($header, '|');
$headerCells = explode('|', $header);
if (count($headerCells) !== count($alignments)) {
return;
}
foreach ($headerCells as $index => $headerCell) {
$headerCell = trim($headerCell);
$HeaderElement = array(
'name' => 'th',
'handler' => array(
'function' => 'lineElements',
'argument' => $headerCell,
'destination' => 'elements',
)
);
if (isset($alignments[$index])) {
$alignment = $alignments[$index];
$HeaderElement['attributes'] = array(
'style' => "text-align: $alignment;",
);
}
$HeaderElements[] = $HeaderElement;
}
$Block = [
'alignments' => $alignments,
'identified' => true,
'element' => [
'name' => 'table',
'elements' => [],
],
];
$Block['element']['elements'][] = [
'name' => 'thead',
];
$Block['element']['elements'][] = [
'name' => 'tbody',
'elements' => [],
];
$Block['element']['elements'][0]['elements'][] = [
'name' => 'tr',
'elements' => $HeaderElements,
];
return $Block;
}
/**
* blockTableContinue
*/
protected function blockTableContinue($Line, array $Block)
{
if (isset($Block['interrupted'])) {
return;
}
if (count($Block['alignments']) === 1 || $Line['text'][0] === '|' || strpos($Line['text'], '|')) {
$Elements = [];
$row = $Line['text'];
$row = trim($row);
$row = trim($row, '|');
preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
$cells = array_slice($matches[0], 0, count($Block['alignments']));
foreach ($cells as $index => $cell) {
$cell = trim($cell);
$Element = array(
'name' => 'td',
'handler' => array(
'function' => 'lineElements',
'argument' => $cell,
'destination' => 'elements',
)
);
if (isset($Block['alignments'][$index])) {
$Element['attributes'] = array(
'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
);
}
$Elements[] = $Element;
}
$Element = array(
'name' => 'tr',
'elements' => $Elements,
);
$Block['element']['elements'][1]['elements'][] = $Element;
return $Block;
}
}
/**
* paragraph
*/
protected function paragraph($Line)
{
return array(
'type' => 'Paragraph',
'element' => array(
'name' => 'p',
'handler' => array(
'function' => 'lineElements',
'argument' => $Line['text'],
'destination' => 'elements',
),
),
);
}
/**
* paragraphContinue
*/
protected function paragraphContinue($Line, array $Block)
{
if (isset($Block['interrupted'])) {
return;
}
$Block['element']['handler']['argument'] .= "\n".$Line['text'];
return $Block;
}
/**
* line
*/
public function line($text, $nonNestables = [])
{
return $this->elements($this->lineElements($text, $nonNestables));
}
/**
* lineElements
*/
protected function lineElements($text, $nonNestables = [])
{
// standardize line breaks
$text = str_replace(array("\r\n", "\r"), "\n", $text);
$Elements = [];
$nonNestables = (empty($nonNestables)
? []
: array_combine($nonNestables, $nonNestables)
);
// $excerpt is based on the first occurrence of a marker
while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
$marker = $excerpt[0];
$markerPosition = strlen($text) - strlen($excerpt);
$Excerpt = ['text' => $excerpt, 'context' => $text];
foreach ($this->InlineTypes[$marker] as $inlineType) {
// Check to see if the current inline type is nestable in the current context
if (isset($nonNestables[$inlineType])) {
continue;
}
$Inline = $this->{"inline$inlineType"}($Excerpt);
if (!isset($Inline)) {
continue;
}
// Make sure that the inline belongs to "our" marker
if (isset($Inline['position']) && $Inline['position'] > $markerPosition) {
continue;
}
// Set a default inline position
if (!isset($Inline['position'])) {
$Inline['position'] = $markerPosition;
}
// Cause the new element to 'inherit' our non nestables
$Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
? array_merge($Inline['element']['nonNestables'], $nonNestables)
: $nonNestables
;
// The text that comes before the inline
$unmarkedText = substr($text, 0, $Inline['position']);
// Compile the unmarked text
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
// Compile the inline
$Elements[] = $this->extractElement($Inline);
// Remove the examined text
$text = substr($text, $Inline['position'] + $Inline['extent']);
continue 2;
}
// The marker does not belong to an inline
$unmarkedText = substr($text, 0, $markerPosition + 1);
$InlineText = $this->inlineText($unmarkedText);
$Elements[] = $InlineText['element'];
$text = substr($text, $markerPosition + 1);
}
$InlineText = $this->inlineText($text);
$Elements[] = $InlineText['element'];
foreach ($Elements as &$Element) {
if (!isset($Element['autobreak'])) {
$Element['autobreak'] = false;
}
}
return $Elements;
}
/**
* inlineText
*/
protected function inlineText($text)
{
$Inline = [
'extent' => strlen($text),
'element' => [],
];
$Inline['element']['elements'] = self::pregReplaceElements(
$this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
[
['name' => 'br'],
['text' => "\n"],
],
$text
);
return $Inline;
}
/**
* inlineCode
*/
protected function inlineCode($Excerpt)
{
$marker = $Excerpt['text'][0];
if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]),
'element' => [
'name' => 'code',
'text' => $text,
],
];
}
}
/**
* inlineEmailTag
*/
protected function inlineEmailTag($Excerpt)
{
$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
if (
strpos($Excerpt['text'], '>') !== false &&
preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
){
$url = $matches[1];
if (!isset($matches[2])) {
$url = "mailto:$url";
}
return [
'extent' => strlen($matches[0]),
'element' => [
'name' => 'a',
'text' => $matches[1],
'attributes' => [
'href' => $url,
],
],
];
}
}
/**
* inlineEmphasis
*/
protected function inlineEmphasis($Excerpt)
{
if (!isset($Excerpt['text'][1])) {
return;
}
$marker = $Excerpt['text'][0];
if ($Excerpt['text'][1] === $marker && preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
$emphasis = 'strong';
}
elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
$emphasis = 'em';
}
else {
return;
}
return [
'extent' => strlen($matches[0]),
'element' => [
'name' => $emphasis,
'handler' => [
'function' => 'lineElements',
'argument' => $matches[1],
'destination' => 'elements',
]
],
];
}
/**
* inlineEscapeSequence
*/
protected function inlineEscapeSequence($Excerpt)
{
if (isset($Excerpt['text'][1]) && in_array($Excerpt['text'][1], $this->specialCharacters)) {
return array(
'element' => array('rawHtml' => $Excerpt['text'][1]),
'extent' => 2,
);
}
}
/**
* inlineImage
*/
protected function inlineImage($Excerpt)
{
if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[') {
return;
}
$Excerpt['text']= substr($Excerpt['text'], 1);
$Link = $this->inlineLink($Excerpt);
if ($Link === null) {
return;
}
$Inline = array(
'extent' => $Link['extent'] + 1,
'element' => array(
'name' => 'img',
'attributes' => array(
'src' => $Link['element']['attributes']['href'],
'alt' => $Link['element']['handler']['argument'],
),
'autobreak' => true,
),
);
$Inline['element']['attributes'] += $Link['element']['attributes'];
unset($Inline['element']['attributes']['href']);
return $Inline;
}
/**
* inlineLink
*/
protected function inlineLink($Excerpt)
{
$Element = array(
'name' => 'a',
'handler' => array(
'function' => 'lineElements',
'argument' => null,
'destination' => 'elements',
),
'nonNestables' => array('Url', 'Link'),
'attributes' => array(
'href' => null,
'title' => null,
),
);
$extent = 0;
$remainder = $Excerpt['text'];
if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
$Element['handler']['argument'] = $matches[1];
$extent += strlen($matches[0]);
$remainder = substr($remainder, $extent);
}
else {
return;
}
if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) {
$Element['attributes']['href'] = $matches[1];
if (isset($matches[2])) {
$Element['attributes']['title'] = substr($matches[2], 1, - 1);
}
$extent += strlen($matches[0]);
}
else {
if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
$definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
$definition = strtolower($definition);
$extent += strlen($matches[0]);
}
else {
$definition = strtolower($Element['handler']['argument']);
}
if (!isset($this->DefinitionData['Reference'][$definition])) {
return;
}
$Definition = $this->DefinitionData['Reference'][$definition];
$Element['attributes']['href'] = $Definition['url'];
$Element['attributes']['title'] = $Definition['title'];
}
return [
'extent' => $extent,
'element' => $Element,
];
}
/**
* inlineMarkup
*/
protected function inlineMarkup($Excerpt)
{
if ($this->markupEscaped || $this->safeMode || strpos($Excerpt['text'], '>') === false) {
return;
}
if ($Excerpt['text'][1] === '/' && preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) {
return array(
'element' => array('rawHtml' => $matches[0]),
'extent' => strlen($matches[0]),
);
}
if ($Excerpt['text'][1] === '!' && preg_match('/^/s', $Excerpt['text'], $matches)) {
return array(
'element' => array('rawHtml' => $matches[0]),
'extent' => strlen($matches[0]),
);
}
if ($Excerpt['text'][1] !== ' ' && preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) {
return array(
'element' => array('rawHtml' => $matches[0]),
'extent' => strlen($matches[0]),
);
}
}
/**
* inlineSpecialCharacter
*/
protected function inlineSpecialCharacter($Excerpt)
{
if (
substr($Excerpt['text'], 1, 1) !== ' ' && strpos($Excerpt['text'], ';') !== false &&
preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
) {
return array(
'element' => array('rawHtml' => '&' . $matches[1] . ';'),
'extent' => strlen($matches[0]),
);
}
return;
}
/**
* inlineStrikethrough
*/
protected function inlineStrikethrough($Excerpt)
{
if (!isset($Excerpt['text'][1])) {
return;
}
if ($Excerpt['text'][1] === '~' && preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'del',
'handler' => array(
'function' => 'lineElements',
'argument' => $matches[1],
'destination' => 'elements',
)
),
);
}
}
/**
* inlineUrl
*/
protected function inlineUrl($Excerpt)
{
if ($this->urlsLinked !== true || ! isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/') {
return;
}
if (strpos($Excerpt['context'], 'http') !== false
&& preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
) {
$url = $matches[0][0];
$Inline = array(
'extent' => strlen($matches[0][0]),
'position' => $matches[0][1],
'element' => array(
'name' => 'a',
'text' => $url,
'attributes' => array(
'href' => $url,
),
),
);
return $Inline;
}
}
/**
* inlineUrlTag
*/
protected function inlineUrlTag($Excerpt)
{
if (strpos($Excerpt['text'], '>') !== false && preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) {
$url = $matches[1];
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'a',
'text' => $url,
'attributes' => array(
'href' => $url,
),
),
);
}
}
/**
* unmarkedText
*/
protected function unmarkedText($text)
{
$Inline = $this->inlineText($text);
return $this->element($Inline['element']);
}
/**
* handle
*/
protected function handle(array $Element)
{
if (isset($Element['handler'])) {
if (!isset($Element['nonNestables'])) {
$Element['nonNestables'] = [];
}
if (is_string($Element['handler'])) {
$function = $Element['handler'];
$argument = $Element['text'];
unset($Element['text']);
$destination = 'rawHtml';
}
else {
$function = $Element['handler']['function'];
$argument = $Element['handler']['argument'];
$destination = $Element['handler']['destination'];
}
$Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
if ($destination === 'handler') {
$Element = $this->handle($Element);
}
unset($Element['handler']);
}
return $Element;
}
/**
* handleElementRecursive
*/
protected function handleElementRecursive(array $Element)
{
return $this->elementApplyRecursive(array($this, 'handle'), $Element);
}
/**
* handleElementsRecursive
*/
protected function handleElementsRecursive(array $Elements)
{
return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
}
/**
* elementApplyRecursive
*/
protected function elementApplyRecursive($closure, array $Element)
{
$Element = call_user_func($closure, $Element);
if (isset($Element['elements'])) {
$Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
}
elseif (isset($Element['element'])) {
$Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
}
return $Element;
}
/**
* elementApplyRecursiveDepthFirst
*/
protected function elementApplyRecursiveDepthFirst($closure, array $Element)
{
if (isset($Element['elements'])) {
$Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
}
elseif (isset($Element['element'])) {
$Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
}
$Element = call_user_func($closure, $Element);
return $Element;
}
/**
* elementsApplyRecursive
*/
protected function elementsApplyRecursive($closure, array $Elements)
{
foreach ($Elements as &$Element) {
$Element = $this->elementApplyRecursive($closure, $Element);
}
return $Elements;
}
/**
* elementsApplyRecursiveDepthFirst
*/
protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
{
foreach ($Elements as &$Element) {
$Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
}
return $Elements;
}
/**
* element
*/
protected function element(array $Element)
{
if ($this->safeMode) {
$Element = $this->sanitiseElement($Element);
}
// identity map if element has no handler
$Element = $this->handle($Element);
$hasName = isset($Element['name']);
$markup = '';
if ($hasName) {
$markup .= '<' . $Element['name'];
if (isset($Element['attributes'])) {
foreach ($Element['attributes'] as $name => $value) {
if ($value === null)
{
continue;
}
$markup .= " $name=\"".self::escape($value).'"';
}
}
}
$permitRawHtml = false;
if (isset($Element['text'])) {
$text = $Element['text'];
}
// Very strongly consider an alternative if you're writing an extension
elseif (isset($Element['rawHtml'])) {
$text = $Element['rawHtml'];
$allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
$permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
}
$hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
if ($hasContent) {
$markup .= $hasName ? '>' : '';
if (isset($Element['elements'])) {
$markup .= $this->elements($Element['elements']);
}
elseif (isset($Element['element'])) {
$markup .= $this->element($Element['element']);
}
else {
if (!$permitRawHtml) {
$markup .= self::escape($text, true);
}
else {
$markup .= $text;
}
}
$markup .= $hasName ? '' . $Element['name'] . '>' : '';
}
elseif ($hasName) {
$markup .= ' />';
}
return $markup;
}
/**
* elements
*/
protected function elements(array $Elements)
{
$markup = '';
$autoBreak = true;
foreach ($Elements as $Element) {
if (empty($Element)) {
continue;
}
$autoBreakNext = (isset($Element['autobreak'])
? $Element['autobreak'] : isset($Element['name'])
);
// (autobreak === false) covers both sides of an element
$autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
$markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
$autoBreak = $autoBreakNext;
}
$markup .= $autoBreak ? "\n" : '';
return $markup;
}
/**
* li
*/
protected function li($lines)
{
$Elements = $this->linesElements($lines);
if (!in_array('', $lines)
&& isset($Elements[0]) && isset($Elements[0]['name'])
&& $Elements[0]['name'] === 'p'
) {
unset($Elements[0]['name']);
}
return $Elements;
}
//
// AST Convenience
//
/**
* Replace occurrences $regexp with $Elements in $text. Return an array of
* elements representing the replacement.
*/
protected static function pregReplaceElements($regexp, $Elements, $text)
{
$newElements = [];
while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) {
$offset = $matches[0][1];
$before = substr($text, 0, $offset);
$after = substr($text, $offset + strlen($matches[0][0]));
$newElements[] = array('text' => $before);
foreach ($Elements as $Element) {
$newElements[] = $Element;
}
$text = $after;
}
$newElements[] = array('text' => $text);
return $newElements;
}
//
// Deprecated Methods
//
/**
* parse
*/
function parse($text)
{
$markup = $this->text($text);
return $markup;
}
/**
* sanitiseElement
*/
protected function sanitiseElement(array $Element)
{
static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
static $safeUrlNameToAtt = array(
'a' => 'href',
'img' => 'src',
);
if (!isset($Element['name'])) {
unset($Element['attributes']);
return $Element;
}
if (isset($safeUrlNameToAtt[$Element['name']])) {
$Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
}
if (!empty($Element['attributes'])) {
foreach ($Element['attributes'] as $att => $val) {
// filter out badly parsed attribute
if (!preg_match($goodAttribute, $att)) {
unset($Element['attributes'][$att]);
}
// dump onevent attribute
elseif (self::striAtStart($att, 'on')) {
unset($Element['attributes'][$att]);
}
}
}
return $Element;
}
/**
* filterUnsafeUrlInAttribute
*/
protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
{
foreach ($this->safeLinksWhitelist as $scheme) {
if (self::striAtStart($Element['attributes'][$attribute], $scheme)) {
return $Element;
}
}
$Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
return $Element;
}
/**
* escape
*/
protected static function escape($text, $allowQuotes = false)
{
return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
}
/**
* striAtStart
*/
protected static function striAtStart($string, $needle)
{
$len = strlen($needle);
if ($len > strlen($string)) {
return false;
}
else {
return strtolower(substr($string, 0, $len)) === strtolower($needle);
}
}
/**
* instance
*/
static function instance($name = 'default')
{
if (isset(self::$instances[$name])) {
return self::$instances[$name];
}
$instance = new static();
self::$instances[$name] = $instance;
return $instance;
}
}
================================================
FILE: src/Parse/Parsedown/ParsedownExtra.php
================================================
BlockTypes[':'][] = 'DefinitionList';
$this->BlockTypes['*'][] = 'Abbreviation';
// identify footnote definitions before reference definitions
array_unshift($this->BlockTypes['['], 'Footnote');
// identify footnote markers before before links
array_unshift($this->InlineTypes['['], 'FootnoteMarker');
}
/**
* text
*/
public function text($text)
{
$Elements = $this->textElements($text);
// convert to markup
$markup = $this->elements($Elements);
// trim line breaks
$markup = trim($markup, "\n");
// merge consecutive dl elements
$markup = preg_replace('/<\/dl>\s+
\s+/', '', $markup);
// add footnotes
if (isset($this->DefinitionData['Footnote']))
{
$Element = $this->buildFootnoteElement();
$markup .= "\n" . $this->element($Element);
}
return $markup;
}
/**
* blockAbbreviation
*/
protected function blockAbbreviation($Line)
{
if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
{
$this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
$Block = array(
'hidden' => true,
);
return $Block;
}
}
/**
* blockFootnote
*/
protected function blockFootnote($Line)
{
if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
{
$Block = array(
'label' => $matches[1],
'text' => $matches[2],
'hidden' => true,
);
return $Block;
}
}
/**
* blockFootnoteContinue
*/
protected function blockFootnoteContinue($Line, $Block)
{
if ($Line['text'][0] === '[' && preg_match('/^\[\^(.+?)\]:/', $Line['text']))
{
return;
}
if (isset($Block['interrupted']))
{
if ($Line['indent'] >= 4)
{
$Block['text'] .= "\n\n" . $Line['text'];
return $Block;
}
}
else
{
$Block['text'] .= "\n" . $Line['text'];
return $Block;
}
}
/**
* blockFootnoteComplete
*/
protected function blockFootnoteComplete($Block)
{
$this->DefinitionData['Footnote'][$Block['label']] = array(
'text' => $Block['text'],
'count' => null,
'number' => null,
);
return $Block;
}
/**
* blockDefinitionList
*/
protected function blockDefinitionList($Line, $Block)
{
if (!isset($Block) || $Block['type'] !== 'Paragraph')
{
return;
}
$Element = array(
'name' => 'dl',
'elements' => [],
);
$terms = explode("\n", $Block['element']['handler']['argument']);
foreach ($terms as $term)
{
$Element['elements'] []= array(
'name' => 'dt',
'handler' => array(
'function' => 'lineElements',
'argument' => $term,
'destination' => 'elements'
),
);
}
$Block['element'] = $Element;
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
/**
* blockDefinitionListContinue
*/
protected function blockDefinitionListContinue($Line, array $Block)
{
if ($Line['text'][0] === ':')
{
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
else
{
if (isset($Block['interrupted']) && $Line['indent'] === 0)
{
return;
}
if (isset($Block['interrupted']))
{
$Block['dd']['handler']['function'] = 'textElements';
$Block['dd']['handler']['argument'] .= "\n\n";
$Block['dd']['handler']['destination'] = 'elements';
unset($Block['interrupted']);
}
$text = substr($Line['body'], min($Line['indent'], 4));
$Block['dd']['handler']['argument'] .= "\n" . $text;
return $Block;
}
}
/**
* blockHeader
*/
protected function blockHeader($Line)
{
$Block = parent::blockHeader($Line);
if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
}
return $Block;
}
/**
* blockMarkup
*/
protected function blockMarkup($Line)
{
if ($this->markupEscaped || $this->safeMode)
{
return;
}
if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
{
$element = strtolower($matches[1]);
if (in_array($element, $this->textLevelElements))
{
return;
}
$Block = array(
'name' => $matches[1],
'depth' => 0,
'element' => array(
'rawHtml' => $Line['text'],
'autobreak' => true,
),
);
$length = strlen($matches[0]);
$remainder = substr($Line['text'], $length);
if (trim($remainder) === '')
{
if (isset($matches[2]) || in_array($matches[1], $this->voidElements))
{
$Block['closed'] = true;
$Block['void'] = true;
}
}
else
{
if (isset($matches[2]) || in_array($matches[1], $this->voidElements))
{
return;
}
if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
{
$Block['closed'] = true;
}
}
return $Block;
}
}
/**
* blockMarkupContinue
*/
protected function blockMarkupContinue($Line, array $Block)
{
if (isset($Block['closed'])) {
return;
}
// Open
if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) {
$Block['depth'] ++;
}
// Close
if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) {
if ($Block['depth'] > 0) {
$Block['depth'] --;
}
else {
$Block['closed'] = true;
}
}
if (isset($Block['interrupted'])) {
$Block['element']['rawHtml'] .= "\n";
unset($Block['interrupted']);
}
$Block['element']['rawHtml'] .= "\n".$Line['body'];
return $Block;
}
/**
* blockMarkupComplete
*/
protected function blockMarkupComplete($Block)
{
if (!isset($Block['void']))
{
$Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);
}
return $Block;
}
/**
* blockSetextHeader
*/
protected function blockSetextHeader($Line, ?array $Block = null)
{
$Block = parent::blockSetextHeader($Line, $Block);
if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
}
return $Block;
}
/**
* inlineFootnoteMarker
*/
protected function inlineFootnoteMarker($Excerpt)
{
if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) {
$name = $matches[1];
if (!isset($this->DefinitionData['Footnote'][$name])) {
return;
}
$this->DefinitionData['Footnote'][$name]['count'] ++;
if (!isset($this->DefinitionData['Footnote'][$name]['number'])) {
// » &
$this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount;
}
$Element = [
'name' => 'sup',
'attributes' => ['id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name],
'element' => [
'name' => 'a',
'attributes' => ['href' => '#fn:'.$name, 'class' => 'footnote-ref'],
'text' => $this->DefinitionData['Footnote'][$name]['number'],
],
];
return [
'extent' => strlen($matches[0]),
'element' => $Element,
];
}
}
/**
* inlineLink
*/
protected function inlineLink($Excerpt)
{
$Link = parent::inlineLink($Excerpt);
$remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';
if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) {
$Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
$Link['extent'] += strlen($matches[0]);
}
return $Link;
}
/**
* insertAbreviation
*/
protected function insertAbreviation(array $Element)
{
if (isset($Element['text'])) {
$Element['elements'] = self::pregReplaceElements(
'/\b'.preg_quote($this->currentAbreviation, '/').'\b/',
array(
array(
'name' => 'abbr',
'attributes' => array(
'title' => $this->currentMeaning,
),
'text' => $this->currentAbreviation,
)
),
$Element['text']
);
unset($Element['text']);
}
return $Element;
}
/**
* inlineText
*/
protected function inlineText($text)
{
$Inline = parent::inlineText($text);
if (isset($this->DefinitionData['Abbreviation']))
{
foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
{
$this->currentAbreviation = $abbreviation;
$this->currentMeaning = $meaning;
$Inline['element'] = $this->elementApplyRecursiveDepthFirst(
array($this, 'insertAbreviation'),
$Inline['element']
);
}
}
return $Inline;
}
/**
* addDdElement
*/
protected function addDdElement(array $Line, array $Block)
{
$text = substr($Line['text'], 1);
$text = trim($text);
unset($Block['dd']);
$Block['dd'] = array(
'name' => 'dd',
'handler' => array(
'function' => 'lineElements',
'argument' => $text,
'destination' => 'elements'
),
);
if (isset($Block['interrupted']))
{
$Block['dd']['handler']['function'] = 'textElements';
unset($Block['interrupted']);
}
$Block['element']['elements'] []= & $Block['dd'];
return $Block;
}
/**
* buildFootnoteElement
*/
protected function buildFootnoteElement()
{
$Element = array(
'name' => 'div',
'attributes' => array('class' => 'footnotes'),
'elements' => array(
array('name' => 'hr'),
array(
'name' => 'ol',
'elements' => [],
),
),
);
uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) {
if (!isset($DefinitionData['number'])) {
continue;
}
$text = $DefinitionData['text'];
$textElements = parent::textElements($text);
$numbers = range(1, $DefinitionData['count']);
$backLinkElements = [];
foreach ($numbers as $number) {
$backLinkElements[] = array('text' => ' ');
$backLinkElements[] = array(
'name' => 'a',
'attributes' => array(
'href' => "#fnref$number:$definitionId",
'rev' => 'footnote',
'class' => 'footnote-backref',
),
'rawHtml' => '↩',
'allowRawHtmlInSafeMode' => true,
'autobreak' => false,
);
}
unset($backLinkElements[0]);
$n = count($textElements) -1;
if ($textElements[$n]['name'] === 'p') {
$backLinkElements = array_merge(
array(
array(
'rawHtml' => ' ',
'allowRawHtmlInSafeMode' => true,
),
),
$backLinkElements
);
unset($textElements[$n]['name']);
$textElements[$n] = array(
'name' => 'p',
'elements' => array_merge(
array($textElements[$n]),
$backLinkElements
),
);
}
else {
$textElements[] = array(
'name' => 'p',
'elements' => $backLinkElements
);
}
$Element['elements'][1]['elements'] []= array(
'name' => 'li',
'attributes' => array('id' => 'fn:'.$definitionId),
'elements' => array_merge(
$textElements
),
);
}
return $Element;
}
/**
* parseAttributeData
*/
protected function parseAttributeData($attributeString)
{
$Data = [];
$attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
foreach ($attributes as $attribute) {
if ($attribute[0] === '#') {
$Data['id'] = substr($attribute, 1);
}
// .
else {
$classes []= substr($attribute, 1);
}
}
if (isset($classes)) {
$Data['class'] = implode(' ', $classes);
}
return $Data;
}
/**
* processTag is recursive
*/
protected function processTag($elementMarkup)
{
libxml_use_internal_errors(true);
// Steer away from using fragments since they mess up encoding
$elementMarkup = '' . $elementMarkup . '';
// Load it up
$DOMDocument = new DOMDocument;
$DOMDocument->encoding = 'UTF-8';
$DOMDocument->loadHTML($elementMarkup, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
// Something went wrong (missing body node)
$bodyNode = $DOMDocument->getElementsByTagName('body')->item(0);
if (!$bodyNode || !$bodyNode->firstChild) {
return $elementMarkup;
}
// Parse the markdown
$elementText = '';
if (
$bodyNode->firstChild->nodeType === XML_ELEMENT_NODE &&
$bodyNode->firstChild->getAttribute('markdown') === '1'
) {
foreach ($bodyNode->firstChild->childNodes as $node) {
$elementText .= $DOMDocument->saveHTML($node);
}
$bodyNode->firstChild->removeAttribute('markdown');
$elementText = "\n".$this->text($elementText)."\n";
}
else {
foreach ($bodyNode->firstChild->childNodes as $node) {
$nodeMarkup = $DOMDocument->saveHTML($node);
if (
$node instanceof DOMElement &&
!in_array($node->nodeName, $this->textLevelElements)
) {
$elementText .= $this->processTag($nodeMarkup);
}
else {
$elementText .= $nodeMarkup;
}
}
}
// Replacement to avoid markup getting encoded
$bodyNode->firstChild->nodeValue = 'placeholder\x1A';
$markup = $DOMDocument->saveHTML($bodyNode->firstChild);
$markup = str_replace('placeholder\x1A', $elementText, $markup);
return $markup;
}
/**
* sortFootnotes
*/
protected function sortFootnotes($A, $B)
{
return $A['number'] - $B['number'];
}
}
================================================
FILE: src/Parse/Syntax/FieldParser.php
================================================
tagPrefix = Arr::get($options, 'tagPrefix', '');
$this->template = $template;
$this->processTemplate($template);
}
}
/**
* Processes repeating tags first, then registered tags and assigns
* the results to local object properties.
* @return void
*/
protected function processTemplate($template)
{
// Process repeaters
list($template, $repeatTags, $repeatfields) = $this->processRepeaterTags($template);
// Process registered tags
list($tags, $fields) = $this->processTags($template);
$this->tags += $tags;
$this->fields += $fields;
/*
* Layer the repeater tags over the standard ones to retain
* the original sort order
*/
foreach ($repeatfields as $field => $params) {
$this->fields[$field] = $params;
}
foreach ($repeatTags as $field => $params) {
$this->tags[$field] = $params;
}
}
/**
* Static helper for new instances of this class.
* @param string $template
* @param array $options
* @return FieldParser
*/
public static function parse($template, $options = [])
{
return new static($template, $options);
}
/**
* Returns all tag strings found in the template
* @return array
*/
public function getTags()
{
return $this->tags;
}
/**
* Returns tag strings for a specific field
* @param string $field
* @return array
*/
public function getFieldTags($field)
{
return $this->tags[$field] ?? [];
}
/**
* Returns all field definitions found in the template
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* Returns defined parameters for a single field
* @param string $field
* @return array
*/
public function getFieldParams($field)
{
return $this->fields[$field] ?? [];
}
/**
* Returns default values for all fields.
* @param array $fields
* @return array
*/
public function getDefaultParams($fields = null)
{
if (is_null($fields)) {
$fields = $this->fields;
}
$defaults = [];
foreach ($fields as $field => $params) {
if (!isset($params['type'])) {
continue;
}
if ($params['type'] === 'repeater') {
$defaults[$field] = [];
$defaults[$field][] = $this->getDefaultParams(Arr::get($params, 'fields', []));
}
else {
$defaults[$field] = $params['default'] ?? null;
}
}
return $defaults;
}
/**
* Processes all repeating tags against a template, this will strip
* any repeaters from the template for further processing.
* @param string $template
* @return void
*/
protected function processRepeaterTags($template)
{
list($tags, $fields) = $this->processTags($template, ['repeater']);
foreach ($fields as $name => &$field) {
$outerTemplate = $tags[$name];
$innerTemplate = $field['default'];
unset($field['default']);
list($innerTags, $innerFields) = $this->processTags($innerTemplate);
list($openTag, $closeTag) = explode($innerTemplate, $outerTemplate);
$field['fields'] = $innerFields;
$tags[$name] = [
'tags' => $innerTags,
'template' => $outerTemplate,
'open' => $openTag,
'close' => $closeTag
];
// Remove the inner content of the repeater
// tag to prevent further parsing
$template = str_replace($outerTemplate, $openTag.$closeTag, $template);
}
return [$template, $tags, $fields];
}
/**
* Processes all registered tags against a template.
* @param string $template
* @param bool $usingTags
* @return void
*/
protected function processTags($template, $usingTags = null)
{
if (!$usingTags) {
$usingTags = $this->registeredTags;
}
if ($this->tagPrefix) {
foreach ($usingTags as $tag) {
$usingTags[] = $this->tagPrefix . $tag;
}
}
$tags = [];
$fields = [];
$result = $this->processTagsRegex($template, $usingTags);
$tagStrings = $result[0];
$tagNames = $result[1];
$paramStrings = $result[2];
foreach ($tagStrings as $key => $tagString) {
$tagName = $tagNames[$key];
$params = $this->processParams($paramStrings[$key], $tagName);
if (isset($params['name'])) {
$name = $params['name'];
unset($params['name']);
}
else {
$name = md5($tagString);
}
if ($tagName === 'variable') {
$params['X_OCTOBER_IS_VARIABLE'] = true;
$tagName = Arr::get($params, 'type', 'text');
}
$params['type'] = $tagName;
// Convert known properties to array
$arrayProps = ['trigger', 'options', 'availableColors'];
foreach ($arrayProps as $prop) {
if (!isset($params[$prop])) {
continue;
}
$params[$prop] = $this->processOptionsToArray($params[$prop]);
}
$tags[$name] = $tagString;
$fields[$name] = $params;
}
return [$tags, $fields];
}
/**
* Processes group 2 from the Tag regex and returns
* an array of captured parameters.
* @param string $value
* @param string $tagName
* @return array
*/
protected function processParams($value, $tagName)
{
$close = Parser::CHAR_CLOSE;
$closePos = strpos($value, $close);
$defaultValue = '';
if ($closePos === false) {
$paramString = $value;
}
elseif (substr($value, -1) === $close) {
$paramString = substr($value, 0, -1);
}
else {
$paramString = substr($value, 0, $closePos);
$defaultValue = trim(substr($value, $closePos + 1));
}
$result = $this->processParamsRegex($paramString);
$paramNames = $result[1];
$paramValues = $result[2];
// Convert all 'true' and 'false' string values to boolean values
foreach ($paramValues as $key => $value) {
if ($value === 'true' || $value === 'false') {
$paramValues[$key] = $value === 'true';
}
}
$params = array_combine($paramNames, $paramValues);
if ($tagName === 'checkbox') {
$params['_content'] = $defaultValue;
}
else {
$params['default'] = $defaultValue;
}
return $params;
}
/**
* processParamsRegex converts parameter string to an array.
*
* In: name="test" comment="This is a test"
* Out: ['name' => 'test', 'comment' => 'This is a test']
*
* @param string $string
* @return array
*/
protected function processParamsRegex($string)
{
/*
* Match key/value pairs
*
* (\w+)="((?:\\.|[^"\\]+)*|[^"]*)"
*/
$regex = '#';
$regex .= '(\w+)'; // Any word
$regex .= '="'; // Equal sign and open quote
$regex .= '('; // Capture
$regex .= '(?:\\\\.|[^"\\\\]+)*'; // Include escaped quotes \"
$regex .= '|[^"]'; // Or anything other than a quote
$regex .= '*)'; // Capture value
$regex .= '"';
$regex .= '#';
preg_match_all($regex, $string, $match);
return $match;
}
/**
* Performs a regex looking for a field type (key) and returns
* an array where:
*
* 0 - The full tag definition, eg: {text name="test"}Foobar{/text}
* 1 - The opening and closing tag name
* 2 - The tag parameters as a string, eg: name="test"} and;
* 2 - The default text inside the tag (optional), eg: Foobar
*
* @param string $string
* @param string $tags
* @return array
*/
protected function processTagsRegex($string, $tags)
{
/*
* Match opening and close tags
*
* {(text|textarea)\s([\S\s]+?){/(?:\1)}
*/
$open = preg_quote(Parser::CHAR_OPEN);
$close = preg_quote(Parser::CHAR_CLOSE);
$tags = implode('|', $tags);
$regex = '#';
$regex .= $open.'('.$tags.')\s'; // Group 1
$regex .= '([\S\s]+?)'; // Group 2 (Non greedy)
$regex .= $open.'/(?:\1)'.$close; // Group X (Not captured)
$regex .= '#';
preg_match_all($regex, $string, $match);
return $match;
}
/**
* Splits an option string to an array.
*
* one|two -> [one, two]
* one:One|two:Two -> [one => 'One', two => 'Two']
* ClassName::method -> [...]
*
* @param string $optionsString
* @return array
*/
protected function processOptionsToArray($optionsString)
{
$result = [];
if (strpos($optionsString, '::') !== false) {
$options = explode('::', $optionsString);
if (
count($options) === 2 &&
class_exists($options[0]) &&
method_exists($options[0], $options[1])
) {
$result = $options[0]::{$options[1]}();
if (!is_array($result)) {
throw new Exception(sprintf(
'Invalid dropdown option array returned by `%s::%s`',
$options[0],
$options[1]
));
}
return $result;
}
}
$options = explode('|', $optionsString);
foreach ($options as $index => $optionStr) {
$parts = explode(':', $optionStr, 2);
if (count($parts) > 1) {
$key = trim($parts[0]);
if (strlen($key)) {
if (!preg_match('/^[0-9a-z-_]+$/i', $key)) {
throw new Exception(sprintf(
'Invalid drop-down option key: %s. Option keys can contain only digits, Latin letters and characters _ and -',
$key
));
}
$result[$key] = trim($parts[1]);
}
else {
$result[$index] = trim($optionStr);
}
}
else {
$result[$index] = trim($optionStr);
}
}
return $result;
}
}
================================================
FILE: src/Parse/Syntax/Parser.php
================================================
template = $template;
$this->varPrefix = Arr::get($options, 'varPrefix', '');
$this->fieldParser = new FieldParser($template, $options);
$textFilters = [
'md' => ['Markdown', 'parse'],
'media' => [\Media\Classes\MediaLibrary::class, 'url'],
];
$this->textParser = new TextParser(['filters' => $textFilters]);
}
}
/**
* Static helper for new instances of this class.
* @param string $template
* @param array $options
* @return self
*/
public static function parse($template, $options = [])
{
return new static($template, $options);
}
/**
* Renders the template fields to their actual values
* @param array $vars
* @param array $options
* @return string
*/
public function render($vars = [], $options = [])
{
$vars = array_replace_recursive($this->getFieldValues(), (array) $vars);
$this->textParser->setOptions($options);
return $this->textParser->parseString($this->toView(), $vars);
}
/**
* Returns the default field values defined in the template
* @return array
*/
public function getFieldValues()
{
return $this->fieldParser->getDefaultParams();
}
/**
* Returns an array of all fields and their options.
* @return array
*/
public function toEditor()
{
return $this->fieldParser->getFields();
}
/**
* Returns the template with fields replaced with Twig markup
* @return string
*/
public function toTwig()
{
return $this->toViewEngine('twig');
}
/**
* toView returns the template with fields replaced with the simple
* template engine used by the TextParser class.
* @return string
*/
public function toView()
{
return $this->toViewEngine('simple');
}
/**
* toViewEngine parses the template to a specific view engine (Twig, Simple)
* @param string $engine
* @return string
*/
protected function toViewEngine($engine)
{
$engine = ucfirst($engine);
$template = $this->template;
$tags = $this->fieldParser->getTags();
foreach ($tags as $field => $tag) {
$template = is_array($tag)
? $this->processRepeatingTag($engine, $template, $field, $tag)
: $this->processTag($engine, $template, $field, $tag);
}
return $template;
}
/**
* processRepeatingTag
*/
protected function processRepeatingTag($engine, $template, $field, $tagDetails)
{
$prefixField = $this->varPrefix.$field;
$params = $this->fieldParser->getFieldParams($field);
$innerFields = Arr::get($params, 'fields', []);
$innerTags = $tagDetails['tags'];
$innerTemplate = $tagDetails['template'];
/*
* Replace all the inner tags
*/
foreach ($innerTags as $innerField => $tagString) {
$innerParams = Arr::get($innerFields, $innerField, []);
$tagReplacement = $this->{'eval'.$engine.'ViewField'}($innerField, $innerParams, 'fields');
$innerTemplate = str_replace($tagString, $tagReplacement, $innerTemplate);
}
/*
* Replace the opening tag
*/
$openTag = Arr::get($tagDetails, 'open', '{repeater}');
$openReplacement = $engine === 'Twig' ? '{% for fields in '.$prefixField.' %}' : '{'.$prefixField.'}';
$openReplacement = $openReplacement . PHP_EOL;
$innerTemplate = str_replace($openTag, $openReplacement, $innerTemplate);
/*
* Replace the closing tag
*/
$closeTag = Arr::get($tagDetails, 'close', '{/repeater}');
$closeReplacement = $engine === 'Twig' ? '{% endfor %}' : '{/'.$prefixField.'}';
$closeReplacement = PHP_EOL . $closeReplacement;
$innerTemplate = str_replace($closeTag, $closeReplacement, $innerTemplate);
$templateString = $tagDetails['template'];
$template = str_replace($templateString, $innerTemplate, $template);
return $template;
}
/**
* processTag
*/
protected function processTag($engine, $template, $field, $tagString)
{
$prefixField = $this->varPrefix.$field;
$params = $this->fieldParser->getFieldParams($field);
$tagReplacement = $this->{'eval'.$engine.'ViewField'}($prefixField, $params);
$template = str_replace($tagString, $tagReplacement, $template);
return $template;
}
/**
* evalTwigViewField processes a field type and converts it to the Twig engine.
* @param string $field
* @param array $params
* @param string $prefix
* @return string
*/
protected function evalTwigViewField($field, $params, $prefix = null)
{
if (isset($params['X_OCTOBER_IS_VARIABLE'])) {
return '';
}
// Used by Twig for loop
if ($prefix) {
$field = $prefix.'.'.$field;
}
$type = $params['type'] ?? 'text';
switch ($type) {
default:
case 'text':
case 'textarea':
$result = '{{ ' . $field . ' }}';
break;
case 'markdown':
$result = '{{ ' . $field . '|md }}';
break;
case 'richeditor':
$result = '{{ ' . $field . '|raw }}';
break;
case 'mediafinder':
$result = '{{ ' . $field . '|media }}';
break;
case 'checkbox':
$result = '{% if ' . $field . ' %}' . $params['_content'] . '{% endif %}';
break;
case 'datepicker':
switch ($params['mode']) {
default:
case 'datetime':
$result = '{{ ' . $field . '|date("Y-m-d H:i:s") }}';
break;
case 'date':
$result = '{{ ' . $field . '|date("Y-m-d") }}';
break;
case 'time':
$result = '{{ ' . $field . '|date("H:i:s") }}';
break;
}
break;
}
return $result;
}
/**
* Processes a field type and converts it to the Simple engine.
* @param string $field
* @param array $params
* @return string
*/
protected function evalSimpleViewField($field, $params, $prefix = null)
{
if (isset($params['X_OCTOBER_IS_VARIABLE'])) {
return '';
}
$type = $params['type'] ?? 'text';
switch ($type) {
case 'markdown':
$result = static::CHAR_OPEN . $field . '|md' . static::CHAR_CLOSE;
break;
case 'mediafinder':
$result = static::CHAR_OPEN . $field . '|media' . static::CHAR_CLOSE;
break;
case 'checkbox':
$result = static::CHAR_OPEN . '?' . $field . static::CHAR_CLOSE;
$result .= $params['_content'];
$result .= static::CHAR_OPEN . '/' . $field . static::CHAR_CLOSE;
break;
default:
$result = static::CHAR_OPEN . $field . static::CHAR_CLOSE;
break;
}
return $result;
}
}
================================================
FILE: src/Parse/Syntax/README.md
================================================
# Rain Dynamic Syntax
Dynamic Syntax is a templating engine that supports two modes of rendering. Parsing template text can produce two results, either a **view** or **editor** mode. Using this template text as an example:
The inner part of the `{text}...{/text}` tags represents the default **view** text, the remaining properties (name and label) are used primarily for the **editor** mode.
## Class usage
Calling `$syntax->render($params)` will render the template:
Our wonderful website
Calling `$syntax->toTwig()` will render as Twig markup:
{{ websiteName }}
Calling `$syntax->toEditor()` will return an array:
'websiteName' => [
'label' => 'Website name',
'default' => 'Our wonderful website',
'type' => 'text'
]
Example
$syntax = Parser::parse('
echo $syntax->render(['websiteName' => 'Your awesome web page']);
## Supported tags
### Text
Renders a single line editor field for smaller blocks of text. The view value is the text entered.
{text name="websiteName" label="Website Name"}Our wonderful website{/text}
### Textarea
Renders a multiple line editor field for larger blocks of text. The view value is the text entered.
{textarea name="websiteDescription" label="Website Description"}This is our vision for things to come{/textarea}
### Dropdown
Renders a dropdown form field.
{dropdown name="dropdown" label="Pick one" options="One|Two"}{/dropdown}
### Radio
Renders a radio form field.
{radio name="radio" label="Thoughts?" options="y:Yes|n:No|m:Maybe"}{/radio}
### Rich editor
Renders a WYSIWYG content editor.
{richeditor name="content" label="Main content"}Default text{/richeditor}
Renders in Twig as
{{ content|raw }}
### Markdown
Renders a Markdown content editor.
{markdown name="content" label="Markdown content"}Default text{/markdown}
Renders in Twig as
{{ content|md }}
### Checkbox
Renders conditional content inside (still under development)
{checkbox name="showHeader" label="Show heading" default="true"}
This content will be shown if the checkbox is ticked
{/checkbox}
Renders in Twig as
{% if checkbox %}
{{ showHeader }}
{% endif %}
### File Upload
Renders a file upload editor field. The view value is the full path to the file.
{fileupload name="logo" label="Logo"}defaultlogo.png{/fileupload}
### Repeater
Renders a repeating section with other fields inside.
{repeater name="content_sections" prompt="Add another content section"}
{/repeater}
Renders in Twig as
{% for fields in repeater %}
{{ fields.title }}
{{ fields.content|raw }}
{% endfor %}
Calling `$syntax->toEditor()` will return a different array for a repeater field:
'repeater' => [
'label' => 'Website name',
'type' => 'repeater'
'fields' => [
'title' => [
'label' => 'Title',
'default' => 'Title',
'type' => 'text'
],
'content' => [
'label' => 'Content',
'default' => 'Content',
'type' => 'textarea'
]
]
]
### Variable
Used for adding fields to editor mode only. This tag will not affect the view mode and will be replaced with an empty string.
{variable type="text" name="websiteName" label="Website Name"}Our wonderful website{/variable}
================================================
FILE: src/Parse/Syntax/SyntaxModelTrait.php
================================================
defineSyntaxRelations();
});
}
/**
* initializeSyntaxModelTrait constructor
*/
public function initializeSyntaxModelTrait()
{
$this->bindEvent('model.beforeReplicate', function() {
$this->defineSyntaxRelations();
});
}
/**
* defineSyntaxRelations defines any relationships (attachments) that this model
* will need based on the field definitions.
*/
public function defineSyntaxRelations()
{
$fields = $this->getSyntaxFields();
if (!is_array($fields)) {
return;
}
foreach ($fields as $field => $params) {
if (!isset($params['type'])) {
continue;
}
if ($params['type'] === 'fileupload') {
$this->attachOne[$field] = \System\Models\File::class;
}
}
}
/**
* getFormSyntaxData prepares the syntax field data for saving.
*/
public function getFormSyntaxData()
{
$data = $this->getSyntaxData();
$fields = $this->getSyntaxFields();
if (!is_array($fields)) {
return $data;
}
foreach ($fields as $field => $params) {
if (!isset($params['type'])) {
continue;
}
// File upload
if ($params['type'] === 'fileupload' && $this->hasRelation($field)) {
if ($this->sessionKey) {
if ($image = $this->$field()->withDeferred($this->sessionKey)->first()) {
$data[$field] = $this->getThumbForImage($image, $params);
}
else {
unset($data[$field]);
}
}
elseif ($this->$field) {
$data[$field] = $this->getThumbForImage($this->$field, $params);
}
}
}
return $data;
}
/**
* getThumbForImage helper to get the perfect sized image.
*/
protected function getThumbForImage($image, $params = [])
{
$imageWidth = Arr::get($params, 'imageWidth');
$imageHeight = Arr::get($params, 'imageHeight');
if ($imageWidth && $imageHeight) {
$path = $image->getThumb($imageWidth, $imageHeight, ['mode' => 'crop']);
}
else {
$path = $image->getPath();
}
if (!Str::startsWith($path, ['//', 'http://', 'https://'])) {
$path = Request::getSchemeAndHttpHost() . $path;
}
return $path;
}
/**
* getFormSyntaxFields prepares the syntax fields for use in a Form builder.
* The array name is added to each field.
* @return array
*/
public function getFormSyntaxFields()
{
$fields = $this->getSyntaxFields();
if (!is_array($fields)) {
return [];
}
$newFields = [];
foreach ($fields as $field => $params) {
if (!isset($params['type'])) {
continue;
}
if ($params['type'] !== 'fileupload') {
$newField = $this->getSyntaxDataColumnName().'['.$field.']';
}
else {
$newField = $field;
}
if ($params['type'] === 'repeater') {
$params['form']['fields'] = Arr::get($params, 'fields', []);
unset($params['fields']);
}
$newFields[$newField] = $params;
}
return $newFields;
}
/**
* makeSyntaxFields processes supplied content and extracts the field definitions
* and default data. It is mixed with the current data and applied
* to the fields and data attributes.
* @param string $content
* @return array
*/
public function makeSyntaxFields($content)
{
$parser = Parser::parse($content);
$fields = $parser->toEditor() ?: [];
$this->setAttribute($this->getSyntaxFieldsColumnName(), $fields);
// Remove fields no longer present and add default values
$currentFields = array_intersect_key((array) $this->getFormSyntaxData(), $parser->getFieldValues());
$currentFields = $currentFields + $parser->getFieldValues();
$this->setAttribute($this->getSyntaxDataColumnName(), $currentFields);
return $fields;
}
/**
* getSyntaxParser
*/
public function getSyntaxParser($content)
{
return Parser::parse($content);
}
/**
* getSyntaxDataColumnName returns the data column name.
* @return string
*/
public function getSyntaxDataColumnName()
{
return defined('static::SYNTAX_DATA') ? static::SYNTAX_DATA : 'syntax_data';
}
/**
* getSyntaxData returns value of the model syntax_data column.
* @return int
*/
public function getSyntaxData()
{
return $this->getAttribute($this->getSyntaxDataColumnName());
}
/**
* getSyntaxFieldsColumnName returns fields column name.
* @return string
*/
public function getSyntaxFieldsColumnName()
{
return defined('static::SYNTAX_FIELDS') ? static::SYNTAX_FIELDS : 'syntax_fields';
}
/**
* getSyntaxFields returns value of the model syntax_fields column.
* @return int
*/
public function getSyntaxFields()
{
return $this->getAttribute($this->getSyntaxFieldsColumnName());
}
}
================================================
FILE: src/Parse/Twig.php
================================================
createTemplate($contents);
return $template->render($vars);
}
}
================================================
FILE: src/Parse/Yaml.php
================================================
parse($contents);
}
/**
* parseFile parses YAML file contents in to a PHP array.
* @param string $fileName File to read contents and parse.
* @return array The YAML contents as an array.
*/
public function parseFile($fileName)
{
$contents = file_get_contents($fileName);
try {
$parsed = $this->parse($contents);
}
catch (Exception $ex) {
throw new ParseException("A syntax error was detected in $fileName. " . $ex->getMessage(), __LINE__, __FILE__);
}
return $parsed;
}
/**
* parseFileCached parses YAML file contents in to a PHP array, with cache.
* @param string $fileName File to read contents and parse.
* @return array The YAML contents as an array.
*/
public function parseFileCached($fileName)
{
try {
$fileCacheKey = 'yaml::' . $fileName . '-' . filemtime($fileName);
return Cache::memo()->remember($fileCacheKey, 43200, function () use ($fileName) {
return $this->parseFile($fileName);
});
}
catch (Exception $ex) {
return $this->parseFile($fileName);
}
}
/**
* render a PHP array to YAML format.
*
* Supported options:
* - inline: The level where you switch to inline YAML.
* - exceptionOnInvalidType: if an exception must be thrown on invalid types.
* - objectSupport: if object support is enabled.
*
* @param array $vars
* @param array $options
* @return string
*/
public function render($vars = [], $options = [])
{
extract(array_merge([
'inline' => 20,
'exceptionOnInvalidType' => false,
'objectSupport' => true,
], $options));
$flags = null;
if ($exceptionOnInvalidType) {
$flags |= YamlComponent::DUMP_EXCEPTION_ON_INVALID_TYPE;
}
if ($objectSupport) {
$flags |= YamlComponent::DUMP_OBJECT;
}
return (new Dumper)->dump($vars, $inline, 0, $flags);
}
}
================================================
FILE: src/Resize/ResizeBuilder.php
================================================
app->singleton('resizer', function ($app) {
return new ResizeBuilder;
});
}
/**
* provides the returned services.
* @return array
*/
public function provides()
{
return ['resizer'];
}
}
================================================
FILE: src/Resize/Resizer.php
================================================
file = $file;
// Get the file extension
$this->extension = $file->guessExtension();
$this->mime = $file->getMimeType();
// Open up the file
$this->image = $this->openImage($file);
// Get width and height of our image
$this->width = $this->image->width();
$this->height = $this->image->height();
// Set default options
$this->setOptions([]);
}
/**
* open is a static constructor
*/
public static function open($file): Resizer
{
return new Resizer($file);
}
/**
* setOptions sets resizer options
*/
public function setOptions(array $options): static
{
$this->options = array_merge([
'mode' => 'auto',
'offset' => [0, 0],
'sharpen' => 0,
'interlace' => false,
'quality' => 90
], $options);
return $this;
}
/**
* getOption gets an individual resizer option
* @param string $option
*/
protected function getOption($option)
{
return $this->options[$option] ?? null;
}
/**
* openImage opens a file, detect its mime-type and create an image resource from it
* @param \Symfony\Component\HttpFoundation\File\File $file
* @return mixed
*/
protected function openImage($file): ImageInterface
{
$filePath = $file->getPathname();
$driver = new \Intervention\Image\Drivers\Gd\Driver;
$manager = new \Intervention\Image\ImageManager($driver);
return $manager->read($filePath);
}
/**
* reset the image back to the original.
*/
public function reset(): static
{
$this->image = $this->openImage($this->file);
return $this;
}
/**
* save the image based on its file type.
* @param string $savePath
*/
public function save($savePath)
{
$this->image->save(
$savePath,
...$this->buildEncoderOptions($savePath)
);
}
/**
* resize and/or crop an image, specifying the new width and height of the
* destination image.
* @param int|null $width
* @param int|null $height
* @param array $options
*/
public function resize($width, $height, $options = []): static
{
$this->setOptions($options);
// Support null for proportional resizing
$width = (int) $width;
$height = (int) $height;
if (!$width && !$height) {
$width = $this->width;
$height = $this->height;
}
elseif (!$width) {
$width = (int) round($height * ($this->width / $this->height));
}
elseif (!$height) {
$height = (int) round($width * ($this->height / $this->width));
}
$mode = $this->options['mode'] ?? 'auto';
if ($mode === 'exact') {
$this->image->resize($width, $height);
}
elseif ($mode === 'crop') {
// Backward compatibility
if (!is_array($options['offset'] ?? null)) {
$this->image->cover($width, $height);
}
else {
$this->image->crop(
$width,
$height,
$this->options['offset'][0] ?? ($this->options['offset']['x'] ?? 0),
$this->options['offset'][1] ?? ($this->options['offset']['y'] ?? 0)
);
}
}
elseif ($mode === 'cover') {
$this->image->cover($width, $height);
}
elseif ($mode === 'fit') {
$this->image->scale($width, $height);
}
elseif ($mode === 'auto') {
$this->image->scale($width, $height);
}
elseif ($mode === 'portrait') {
$this->image->scale(null, $height);
}
elseif ($mode === 'landscape') {
$this->image->scale($width, null);
}
$sharpen = $this->getOption('sharpen');
if ($sharpen > 0) {
$this->image->sharpen($sharpen);
}
return $this;
}
/**
* buildEncoderOptions builds encoder options (quality, interlace/progressive) for saving.
* @param string $savePath
*/
protected function buildEncoderOptions(string $savePath): array
{
$encoderOptions = [];
$quality = $this->getOption('quality');
if ($quality !== null) {
$encoderOptions['quality'] = (int) $quality;
}
// Interlace option maps to 'interlaced' for PNG/GIF and 'progressive' for JPEG
$interlace = $this->getOption('interlace');
if ($interlace) {
$extension = strtolower(pathinfo($savePath, PATHINFO_EXTENSION));
if ($extension === 'jpg' || $extension === 'jpeg') {
$encoderOptions['progressive'] = true;
}
elseif ($extension === 'png' || $extension === 'gif') {
$encoderOptions['interlaced'] = true;
}
}
return $encoderOptions;
}
}
================================================
FILE: src/Router/CoreRedirector.php
================================================
session->pull('url.cms.intended', $default);
return $this->to($path, $status, $headers, $secure);
}
/**
* getIntendedUrl from the session.
*/
public function getIntendedUrl()
{
if (!App::runningInFrontend()) {
return parent::getIntendedUrl();
}
return $this->session->get('url.cms.intended');
}
/**
* setIntendedUrl in the session.
*/
public function setIntendedUrl($url)
{
if (!App::runningInFrontend()) {
return parent::setIntendedUrl($url);
}
$this->session->put('url.cms.intended', $url);
return $this;
}
}
================================================
FILE: src/Router/CoreRouter.php
================================================
currentRequest = $request;
$this->events->dispatch('router.before', [$request]);
$response = $this->dispatchToRoute($request);
$this->events->dispatch('router.after', [$request, $response]);
return $response;
}
/**
* before is a new filter registered with the router.
*
* @param string|callable $callback
* @return void
*/
public function before($callback)
{
$this->events->listen('router.before', $callback);
}
/**
* after is a new filter registered with the router.
*
* @param string|callable $callback
* @return void
*/
public function after($callback)
{
$this->events->listen('router.after', $callback);
}
/**
* registerLateRoutes found within "before" filter, some are registered here.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function registerLateRoutes()
{
if (!$this->routerEventsBooted) {
$this->events->dispatch('router.before', [new Request]);
}
$this->routerEventsBooted = true;
return $this;
}
}
================================================
FILE: src/Router/Helper.php
================================================
/some/link/1/Joe
*
* @param stdObject $object Object containing the data
* @param array $columns Expected key names to parse
* @param string $string URL template
* @return string Built string
*/
public static function parseValues($object, array $columns, $string)
{
if (is_array($object)) {
$object = (object) $object;
}
foreach ($columns as $column) {
if (
!isset($object->{$column}) ||
is_array($object->{$column}) ||
(is_object($object->{$column}) && !method_exists($object->{$column}, '__toString'))
) {
continue;
}
$string = str_replace(':'.$column, urlencode((string) $object->{$column}), $string);
}
return $string;
}
/**
* replaceParameters replaces :column_name with object value without requiring a
* list of names. Example: /some/link/:id/:name -> /some/link/1/Joe
*
* @param stdObject $object Object containing the data
* @param string $string URL template
* @return string Built string
*/
public static function replaceParameters($object, $string)
{
if (preg_match_all('/\:([\w]+)/', $string, $matches)) {
return self::parseValues($object, $matches[1], $string);
}
return $string;
}
/**
* segmentIsWildcard checks whether an URL pattern segment is a wildcard.
* @param string $segment The segment definition.
* @return boolean Returns boolean true if the segment is a wildcard. Returns false otherwise.
*/
public static function segmentIsWildcard($segment)
{
$name = mb_substr($segment, 1);
$wildMarkerPos = mb_strpos($name, '*');
if ($wildMarkerPos === false) {
return false;
}
$regexMarkerPos = mb_strpos($name, '|');
if ($regexMarkerPos === false) {
return true;
}
if ($wildMarkerPos !== false && $regexMarkerPos !== false) {
return $wildMarkerPos < $regexMarkerPos;
}
return true;
}
/**
* segmentIsOptional checks whether an URL pattern segment is optional.
* @param string $segment The segment definition.
* @return boolean Returns boolean true if the segment is optional. Returns false otherwise.
*/
public static function segmentIsOptional($segment)
{
$name = mb_substr($segment, 1);
$optMarkerPos = mb_strpos($name, '?');
if ($optMarkerPos === false) {
return false;
}
$regexMarkerPos = mb_strpos($name, '|');
if ($regexMarkerPos === false) {
return true;
}
if ($optMarkerPos !== false && $regexMarkerPos !== false) {
return $optMarkerPos < $regexMarkerPos;
}
return false;
}
/**
* getParameterName extracts the parameter name from a URL pattern segment definition.
* @param string $segment
* @return string
*/
public static function getParameterName($segment)
{
$name = mb_substr($segment, 1);
$regexMarkerPos = mb_strpos($name, '|');
if ($regexMarkerPos !== false) {
$name = mb_substr($name, 0, $regexMarkerPos);
}
$optMarkerPos = mb_strpos($name, '?');
if ($optMarkerPos !== false) {
$name = mb_substr($name, 0, $optMarkerPos);
}
$wildMarkerPos = mb_strpos($name, '*');
if ($wildMarkerPos !== false) {
$name = mb_substr($name, 0, $wildMarkerPos);
}
return $name;
}
/**
* getSegmentRegExp extracts the regular expression from a URL pattern segment definition.
* @param string $segment The segment definition.
* @return string Returns the regular expression string or false if the expression is not defined.
*/
public static function getSegmentRegExp($segment)
{
if (($pos = mb_strpos($segment, '|')) !== false) {
$regexp = mb_substr($segment, $pos+1);
if (!mb_strlen($regexp)) {
return false;
}
return '/'.$regexp.'/';
}
return false;
}
/**
* getSegmentDefaultValue extracts the default parameter value from a URL pattern
* segment definition.
* @param string $segment The segment definition.
* @return string Returns the default value if it is provided. Returns false otherwise.
*/
public static function getSegmentDefaultValue($segment)
{
$regexMarkerPos = mb_strpos($segment, '|');
// Find the '?' that acts as the optional marker, not one inside a regex
$optMarkerPos = mb_strpos($segment, '?');
if ($optMarkerPos === false) {
return false;
}
// If '|' comes before '?', the '?' is part of the regex, not an optional marker
if ($regexMarkerPos !== false && $regexMarkerPos < $optMarkerPos) {
return false;
}
$value = false;
if ($regexMarkerPos !== false) {
$value = mb_substr($segment, $optMarkerPos+1, $regexMarkerPos-$optMarkerPos-1);
}
else {
$value = mb_substr($segment, $optMarkerPos+1);
}
// Filter out wildcard marker (it's a modifier, not a default value)
if ($value === '*') {
return false;
}
return strlen($value) ? $value : false;
}
}
================================================
FILE: src/Router/README.md
================================================
# URL Router
URL route patterns follow an easy to read syntax and use in-place named parameters, so there is no need to use regular expressions in most cases.
## Creating a route
You should prepare your route like so:
```php
$router = new Router;
// New route with ID: myRouteId
$router->route('myRouteId', '/post/:id');
// New route with ID: anotherRouteId
$router->route('anotherRouteId', '/profile/:username');
```
## Route matching
Once you have prepared your route you can match it like this:
```php
if ($router->match('/post/2')) {
// Returns: [id => 2]
$params = $router->getParameters();
// Returns: myRouteId
$routeId = $router->matchedRoute();
}
```
## Reverse matching
You can also reverse match a route by it's identifier:
```php
// Returns: /post/2
$url = $router->url('myRouteId', ['id' => 2]);
```
================================================
FILE: src/Router/Router.php
================================================
routeMap[$name] = Rule::fromPattern($name, $route);
}
/**
* match given URL string
* @param string $url Request URL to match for
* @return bool
*/
public function match($url)
{
// Reset any previous matches
$this->matchedRouteRule = null;
$segments = Helper::segmentizeUrl($url, false);
$parameters = [];
foreach ($this->routeMap as $routeRule) {
if ($routeRule->resolveUrlSegments($segments, $parameters)) {
$this->matchedRouteRule = $routeRule;
// If this route has a condition, run it
$callback = $routeRule->condition();
if ($callback !== null) {
$callbackResult = call_user_func($callback, $parameters, Helper::normalizeUrl($url));
// Callback responded to abort
if ($callbackResult === false) {
$parameters = [];
$this->matchedRouteRule = null;
continue;
}
}
break;
}
}
// Success
if ($this->matchedRouteRule) {
// If this route has a match callback, run it
$matchCallback = $routeRule->afterMatch();
if ($matchCallback !== null) {
$parameters = call_user_func($matchCallback, $parameters, $url);
}
}
$this->parameters = $parameters;
return $this->matchedRouteRule ? true : false;
}
/**
* url builds a URL together by matching route name and supplied parameters
*
* @param string $name Name of the route previously defined.
* @param array $parameters Parameter name => value items to fill in for given route.
* @return string Full matched URL as string with given values put in place of named parameters
*/
public function url($name, $parameters = [])
{
if (!isset($this->routeMap[$name])) {
return null;
}
$routeRule = $this->routeMap[$name];
$pattern = $routeRule->pattern();
return $this->urlFromPattern($pattern, $parameters);
}
/**
* urlFromPattern builds a URL together by matching route pattern and supplied parameters
*
* @param string $pattern Route pattern string, eg: /path/to/something/:parameter
* @param array $parameters Parameter name => value items to fill in for given route.
* @return string Full matched URL as string with given values put in place of named parameters
*/
public function urlFromPattern($pattern, $parameters = [])
{
$patternSegments = Helper::segmentizeUrl($pattern);
// Normalize the parameters, colons (:) in key names are removed.
//
foreach ($parameters as $param => $value) {
if (strpos($param, ':') !== 0) {
continue;
}
$normalizedParam = substr($param, 1);
$parameters[$normalizedParam] = $value;
unset($parameters[$param]);
}
// Build the URL segments, remember the last populated index
//
$url = [];
$lastPopulatedIndex = 0;
foreach ($patternSegments as $index => $patternSegment) {
// Static segment
if (strpos($patternSegment, ':') !== 0) {
$url[] = $patternSegment;
}
// Dynamic segment
else {
$paramName = Helper::getParameterName($patternSegment);
// Determine whether it is optional
$optional = Helper::segmentIsOptional($patternSegment);
// Default value
$defaultValue = Helper::getSegmentDefaultValue($patternSegment);
// Check if parameter has been supplied and is not a default value
$parameterExists = isset($parameters[$paramName]) &&
strlen($parameters[$paramName]) &&
$parameters[$paramName] !== $defaultValue;
// Use supplied parameter value
if ($parameterExists) {
$url[] = $parameters[$paramName];
}
// Look for a specified default value
elseif ($optional) {
$url[] = $defaultValue ?: static::$defaultValue;
// Do not set $lastPopulatedIndex
continue;
}
// Non optional field, use the default value
else {
$url[] = static::$defaultValue;
}
}
$lastPopulatedIndex = $index;
}
// Trim the URL to only include populated segments
$url = array_slice($url, 0, $lastPopulatedIndex + 1);
return Helper::rebuildUrl($url);
}
/**
* getRouteMap returns the active list of router rule objects
* @return array An associative array with keys matching the route rule names and
* values matching the router rule object.
*/
public function getRouteMap()
{
return $this->routeMap;
}
/**
* getParameters returns a list of parameters specified in the requested page URL.
* For example, if the URL pattern was /blog/post/:id and the actual URL
* was /blog/post/10, the $parameters['id'] element would be 10.
* @return array An associative array with keys matching the parameter names specified in the URL pattern and
* values matching the corresponding segments of the actual requested URL.
*/
public function getParameters()
{
return $this->parameters;
}
/**
* matchedRoute returns the matched route rule name.
* @return \October\Rain\Router\Rule The matched rule object.
*/
public function matchedRoute()
{
if (!$this->matchedRouteRule) {
return false;
}
return $this->matchedRouteRule->name();
}
/**
* reset clears all existing routes
* @return $this
*/
public function reset()
{
$this->routeMap = [];
return $this;
}
/**
* sortRules sorts all the routing rules by static segments (long to short),
* then dynamic segments (short to long), then wild segments (at end).
* @return void
*/
public function sortRules()
{
uasort($this->routeMap, function ($a, $b) {
// When comparing static, longer tails go to the start
$lengthA = $a->staticSegmentCount;
$lengthB = $b->staticSegmentCount;
if ($lengthA > $lengthB) {
return -1;
}
if ($lengthA < $lengthB) {
return 1;
}
// When static tails are equal, push wilds to the end
$lengthA = $a->wildSegmentCount;
$lengthB = $b->wildSegmentCount;
if ($lengthA > $lengthB) {
return 1;
}
if ($lengthA < $lengthB) {
return -1;
}
// When comparing dynamic, longer tails go to the end
$lengthA = $a->dynamicSegmentCount;
$lengthB = $b->dynamicSegmentCount;
if ($lengthA > $lengthB) {
return 1;
}
if ($lengthA < $lengthB) {
return -1;
}
return 0;
});
}
/**
* fromArray loads routes from an array.
*/
public function fromArray($routes)
{
foreach ($routes as $route) {
$this->routeMap[$route['ruleName']] = new Rule($route);
}
}
/**
* toArray converts the rules to an array.
* @return array
*/
public function toArray()
{
$this->sortRules();
$rules = [];
foreach ($this->routeMap as $rule) {
$rules[] = $rule->toArray();
}
return $rules;
}
}
================================================
FILE: src/Router/RoutingServiceProvider.php
================================================
app->routesAreCached()) {
$this->loadCachedRoutes();
}
else {
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
}
/**
* loadCachedRoutes for the application.
*/
protected function loadCachedRoutes()
{
$this->app->booted(function () {
require $this->app->getCachedRoutesPath();
});
}
/**
* registerRouter instance.
*/
protected function registerRouter()
{
$this->app->singleton('router', function ($app) {
return new CoreRouter($app['events'], $app);
});
}
/**
* registerRedirector
*/
protected function registerRedirector()
{
$this->app->singleton('redirect', function ($app) {
$redirector = new CoreRedirector($app['url']);
// If the session is set on the application instance, we'll inject it into
// the redirector instance. This allows the redirect responses to allow
// for the quite convenient "with" methods that flash to the session.
if (isset($app['session.store'])) {
$redirector->setSession($app['session.store']);
}
return $redirector;
});
}
}
================================================
FILE: src/Router/Rule.php
================================================
config = $config;
foreach ($config as $key => $val) {
$this->{$key} = $val;
}
}
/**
* fromPattern returns a named rule from a pattern
*/
public static function fromPattern($name, $pattern): static
{
$segments = Helper::segmentizeUrl($pattern);
// Create the static URL for this pattern for reverse lookup
//
$staticSegments = [];
$staticSegmentCount = $dynamicSegmentCount = $wildSegmentCount = 0;
foreach ($segments as $segment) {
if (strpos($segment, ':') !== 0) {
$staticSegments[] = $segment;
$staticSegmentCount++;
}
else {
$dynamicSegmentCount++;
if (Helper::segmentIsWildcard($segment)) {
$wildSegmentCount++;
}
}
}
$staticUrl = Helper::rebuildUrl($staticSegments);
// Build and return rule
//
$rule = new static([
'ruleName' => $name,
'rulePattern' => $pattern,
'segments' => $segments,
'segmentCount' => count($segments),
'staticUrl' => $staticUrl,
'staticSegments' => $staticSegments,
'staticSegmentCount' => $staticSegmentCount,
'dynamicSegmentCount' => $dynamicSegmentCount,
'wildSegmentCount' => $wildSegmentCount,
]);
return $rule;
}
/**
* resolveUrl checks whether a given URL matches a given pattern, with a reference to a PHP array
* variable to return the parameter list fetched from URL. Returns true if the URL matches the
* pattern. Otherwise returns false.
* @param string $url
* @param array $parameters
* @return bool
*/
public function resolveUrl($url, &$parameters)
{
return $this->resolveUrlSegments(Helper::segmentizeUrl($url), $parameters);
}
/**
* resolveUrlSegments is an internal method used for multiple checks.
* @param array $urlSegments
* @param array $parameters
* @return bool
*/
public function resolveUrlSegments($urlSegments, &$parameters)
{
$parameters = [];
// Only one wildcard can be used, if found, pull out the excess segments
$wildSegments = [];
if ($this->wildSegmentCount === 1) {
$wildSegments = $this->captureWildcardSegments($urlSegments);
}
// If the number of URL segments is more than the number of pattern segments
if (count($urlSegments) > $this->segmentCount) {
return false;
}
// Compare pattern and URL segments
foreach ($this->segments as $index => $patternSegment) {
// Static segment
if (strpos($patternSegment, ':') !== 0) {
if (
!array_key_exists($index, $urlSegments) ||
mb_strtolower($patternSegment) !== mb_strtolower($urlSegments[$index])
) {
return false;
}
}
// Dynamic segment
else {
// Initialize the parameter
$paramName = Helper::getParameterName($patternSegment);
$parameters[$paramName] = false;
// Determine whether it is optional
$optional = Helper::segmentIsOptional($patternSegment);
// Check if the optional segment has no required segments following it
if ($optional && $index < ($this->segmentCount - 1)) {
for ($i = $index+1; $i < $this->segmentCount; $i++) {
if (!Helper::segmentIsOptional($this->segments[$i])) {
$optional = false;
break;
}
}
}
// If the segment is optional and there is no corresponding value in the URL,
// assign the default value (if provided) and skip to the next segment.
$urlSegmentExists = array_key_exists($index, $urlSegments);
if ($optional && !$urlSegmentExists) {
$parameters[$paramName] = Helper::getSegmentDefaultValue($patternSegment);
continue;
}
// If the segment is not optional and there is no corresponding value in the URL
if (!$optional && !$urlSegmentExists) {
return false;
}
// Validate the value with the regular expression
$regexp = Helper::getSegmentRegExp($patternSegment);
if ($regexp) {
try {
if (!preg_match($regexp, $urlSegments[$index])) {
return false;
}
}
catch (Exception $ex) {
}
}
// Set the parameter value
$parameters[$paramName] = $urlSegments[$index];
// Determine if wildcard and add stored parameters as a suffix
if (Helper::segmentIsWildcard($patternSegment) && count($wildSegments)) {
$parameters[$paramName] .= Helper::rebuildUrl($wildSegments);
}
}
}
return true;
}
/**
* captureWildcardSegments captures and removes every segment of a URL after a wildcard
* pattern segment is detected, until both collections of segments
* are the same size.
* @param array $urlSegments
* @return array
*/
protected function captureWildcardSegments(&$urlSegments)
{
$wildSegments = [];
$patternSegments = $this->segments;
$segmentDiff = count($urlSegments) - count($patternSegments);
$wildMode = false;
$wildCount = 0;
foreach ($urlSegments as $index => $urlSegment) {
if ($wildMode) {
if ($wildCount < $segmentDiff) {
$wildSegments[] = $urlSegment;
$wildCount++;
unset($urlSegments[$index]);
continue;
}
break;
}
$patternSegment = $patternSegments[$index];
if (Helper::segmentIsWildcard($patternSegment)) {
$wildMode = true;
}
}
// Reset array index
$urlSegments = array_values($urlSegments);
return $wildSegments;
}
/**
* name is a unique route name
*
* @param string $name Unique name for the router object
* @return object Self
*/
public function name($name = null)
{
if ($name === null) {
return $this->ruleName;
}
$this->ruleName = $name;
return $this;
}
/**
* pattern for the route match
*
* @param string $pattern Pattern used to match this rule
* @return self
*/
public function pattern($pattern = null)
{
if ($pattern === null) {
return $this->rulePattern;
}
$this->rulePattern = $pattern;
return $this;
}
/**
* condition callback
*
* @param callback $callback Callback function to be used when providing custom route match conditions
* @throws InvalidArgumentException When supplied argument is not a valid callback
* @return callback
*/
public function condition($callback = null)
{
if ($callback !== null) {
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
"Condition provided is not a valid callback. Given (%s)",
gettype($callback)
));
}
$this->conditionCallback = $callback;
return $this;
}
return $this->conditionCallback;
}
/**
* afterMatch callback
*
* @param callback $callback Callback function to be used to modify params after a successful match
* @throws InvalidArgumentException When supplied argument is not a valid callback
* @return callback
*/
public function afterMatch($callback = null)
{
if ($callback !== null) {
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
"The after match callback provided is not valid. Given (%s)",
gettype($callback)
));
}
$this->afterMatchCallback = $callback;
return $this;
}
return $this->afterMatchCallback;
}
/**
* toArray
*/
public function toArray()
{
return $this->config;
}
}
================================================
FILE: src/Scaffold/Console/CreateCommand.php
================================================
(eg: Acme.Blog)}
{name : The name of the command. Eg: ProcessJobs}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new console command.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Command';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('command/command.stub', 'console/{{studly_name}}.php');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateComponent.php
================================================
(eg: Acme.Blog)}
{name : The name of the component. Eg: Posts}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string name of console command
*/
protected $name = 'create:component';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new plugin component.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Component';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('component/component.stub', 'components/{{studly_name}}.php');
$this->makeStub('component/default.stub', 'components/{{lower_name}}/default.htm');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateContentField.php
================================================
(eg: Acme.Blog)}
{name : The name of the content field. Eg: IconPicker}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new content field.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Content Field';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('contentfield/contentfield.stub', 'contentfields/{{studly_name}}.php');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateController.php
================================================
(eg: Acme.Blog)}
{name : The name of the controller. Eg: Posts}
{--model= : Define which model name to use, otherwise the singular controller name is used.}
{--design= : Specify a design (basic, sidebar, survey, popup, custom)}
{--no-form : Do not implement a form for this controller}
{--no-list : Do not implement a list for this controller}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new controller.';
/**
* @var string typeLabel of class being generated
*/
protected $typeLabel = 'Controller';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$design = $this->defineDesignMode();
$this->makeStub('controller/controller.stub', 'controllers/{{studly_name}}.php');
if (!$this->option('no-list')) {
$this->makeStub('controller/config_list.stub', 'controllers/{{lower_name}}/config_list.yaml');
$this->makeStub('controller/_list_toolbar.stub', 'controllers/{{lower_name}}/_list_toolbar.php');
$this->makeStub('controller/index.stub', 'controllers/{{lower_name}}/index.php');
}
if (!$this->option('no-form')) {
$this->makeStub('controller/config_form.stub', 'controllers/{{lower_name}}/config_form.yaml');
if (in_array($design, ['basic', 'sidebar', 'survey'])) {
$this->makeStub('controller/update_design.stub', 'controllers/{{lower_name}}/update.php');
$this->makeStub('controller/preview_design.stub', 'controllers/{{lower_name}}/preview.php');
$this->makeStub('controller/create_design.stub', 'controllers/{{lower_name}}/create.php');
}
elseif ($design !== 'popup') {
$this->makeStub('controller/update.stub', 'controllers/{{lower_name}}/update.php');
$this->makeStub('controller/preview.stub', 'controllers/{{lower_name}}/preview.php');
$this->makeStub('controller/create.stub', 'controllers/{{lower_name}}/create.php');
}
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
'model' => $this->defineModelName(),
'design' => $this->defineDesignMode(),
'form' => !$this->option('no-form'),
'list' => !$this->option('no-list'),
];
}
/**
* defineModelName to use, either supplied or singular from the controller name
*/
protected function defineModelName(): string
{
$model = $this->option('model');
if (!$model) {
$model = Str::singular($this->argument('name'));
}
return $model;
}
/**
* defineDesignMode
*/
protected function defineDesignMode(): string
{
if ($design = $this->option('design')) {
return trim(strtolower($design));
}
return '';
}
}
================================================
FILE: src/Scaffold/Console/CreateFactory.php
================================================
(eg: Acme.Blog)}
{name : The name of the factory class to generate. (eg: PostFactory)}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new factory class.';
/**
* @var string typeLabel of class being generated
*/
protected $typeLabel = 'Factory';
/**
* handle executes the console command
*/
public function handle()
{
if (!str_ends_with($this->argument('name'), 'Factory')) {
$this->components->error('Factory classes names must end in "Factory"');
return;
}
parent::handle();
}
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
if ($this->isAppNamespace()) {
$this->makeStub('factory/factory_app.stub', 'database/factories/{{studly_name}}.php');
}
else {
$this->makeStub('factory/factory.stub', 'updates/factories/{{studly_name}}.php');
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateFilterWidget.php
================================================
(eg: Acme.Blog)}
{name : The name of the filter widget. Eg: HasDiscount}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string name of console command
*/
protected $name = 'create:filterwidget';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new filter widget.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Filter Widget';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('filterwidget/filterwidget.stub', 'filterwidgets/{{studly_name}}.php');
$this->makeStub('filterwidget/partial.stub', 'filterwidgets/{{lower_name}}/partials/_{{lower_name}}.php');
$this->makeStub('filterwidget/partial_form.stub', 'filterwidgets/{{lower_name}}/partials/_{{lower_name}}_form.php');
$this->makeStub('filterwidget/stylesheet.stub', 'filterwidgets/{{lower_name}}/assets/css/{{lower_name}}.css');
$this->makeStub('filterwidget/javascript.stub', 'filterwidgets/{{lower_name}}/assets/js/{{lower_name}}.js');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateFormWidget.php
================================================
(eg: Acme.Blog)}
{name : The name of the form widget. Eg: PostList}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new form widget.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Form Widget';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('formwidget/formwidget.stub', 'formwidgets/{{studly_name}}.php');
$this->makeStub('formwidget/partial.stub', 'formwidgets/{{lower_name}}/partials/_{{lower_name}}.php');
$this->makeStub('formwidget/stylesheet.stub', 'formwidgets/{{lower_name}}/assets/css/{{lower_name}}.css');
$this->makeStub('formwidget/javascript.stub', 'formwidgets/{{lower_name}}/assets/js/{{lower_name}}.js');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateJob.php
================================================
(eg: Acme.Blog)}
{name : The name of the job class to generate. (eg: ImportPosts)}
{--s|sync : Indicates that job should be synchronous}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new job class.';
/**
* @var string typeLabel of class being generated
*/
protected $typeLabel = 'Job';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
if ($this->option('sync')) {
$this->makeStub('job/job.stub', 'jobs/{{studly_name}}.php');
}
else {
$this->makeStub('job/job.queued.stub', 'jobs/{{studly_name}}.php');
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateMigration.php
================================================
(eg: Acme.Blog)}
{name : The name of the model. Eg: Post}
{--create= : The table to be created}
{--table= : The table to migrate}
{--soft-deletes : Implement soft deletion on this model}
{--no-timestamps : Disable auto-timestamps on this model}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new migration.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Migration';
/**
* @var bool isCreate determines if this is a creation migration
*/
protected $isCreate = false;
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
if ($this->isAppNamespace()) {
if ($this->isCreate) {
$this->makeStub('migration/create_app_table.stub', 'database/migrations/'.$this->getDatePrefix().'_{{snake_name}}.php');
}
else {
$this->makeStub('migration/update_app_table.stub', 'database/migrations/'.$this->getDatePrefix().'_{{snake_name}}.php');
}
}
else {
if ($this->isCreate) {
$this->makeStub('migration/create_table.stub', 'updates/{{snake_name}}.php');
}
else {
$this->makeStub('migration/update_table.stub', 'updates/{{snake_name}}.php');
}
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
'table' => $this->defineTableName(),
'softDeletes' => $this->option('soft-deletes'),
'timestamps' => !$this->option('no-timestamps')
];
}
/**
* defineTableName
*/
protected function defineTableName(): string
{
if ($table = $this->option('table')) {
return $table;
}
if ($table = $this->option('create')) {
$this->isCreate = true;
return $table;
}
return $this->guessTableName();
}
/**
* guessTableName
*/
protected function guessTableName(): string
{
$tableName = Str::snake($this->argument('name'));
$createPatterns = [
'/^create_(\w+)_table$/',
'/^create_(\w+)$/',
];
foreach ($createPatterns as $pattern) {
if (preg_match($pattern, $tableName, $matches)) {
$tableName = $matches[1];
$this->isCreate = true;
}
}
$updatePatterns = [
'/_(to|from|in)_(\w+)_table$/',
'/_(to|from|in)_(\w+)$/',
];
foreach ($updatePatterns as $pattern) {
if (preg_match($pattern, $tableName, $matches)) {
$tableName = $matches[1];
}
}
return $this->getNamespaceTable() . '_' .$tableName;
}
/**
* getDatePrefix
* @return string
*/
protected function getDatePrefix()
{
return date('Y_m_d_His');
}
}
================================================
FILE: src/Scaffold/Console/CreateModel.php
================================================
(eg: Acme.Blog)}
{name : The name of the model. Eg: Post}
{--soft-deletes : Implement soft deletion on this model}
{--no-timestamps : Disable auto-timestamps on this model}
{--no-migration : Do not generate a migration file for this model}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new model.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Model';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('model/model.stub', 'models/{{studly_name}}.php');
$this->makeStub('model/fields.stub', 'models/{{lower_name}}/fields.yaml');
$this->makeStub('model/columns.stub', 'models/{{lower_name}}/columns.yaml');
if (!$this->option('no-migration')) {
$this->call('create:migration', array_filter([
'name' => 'Create'.$this->vars['studly_plural_name'].'Table',
'namespace' => $this->argument('namespace'),
'--create' => $this->vars['namespace_table'].'_'.$this->vars['snake_plural_name'],
'--soft-deletes' => $this->option('soft-deletes'),
'--no-timestamps' => $this->option('no-timestamps'),
'--overwrite' => $this->option('overwrite')
]));
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
'softDeletes' => $this->option('soft-deletes'),
'timestamps' => !$this->option('no-timestamps')
];
}
}
================================================
FILE: src/Scaffold/Console/CreatePlugin.php
================================================
(eg: Acme.Blog)}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new plugin.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Plugin';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('plugin/plugin.stub', 'Plugin.php');
$this->makeStub('plugin/version.stub', 'updates/version.yaml');
$this->makeStub('plugin/composer.stub', 'composer.json');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
if (!$this->validateInput()) {
exit(1);
}
return [
'namespace' => $this->argument('namespace'),
];
}
protected function validateInput()
{
if ($this->isAppNamespace()) {
$this->error('Cannot create plugin in app namespace');
return false;
}
// Extract the author and name from the plugin code
$pluginCode = $this->argument('namespace');
$parts = explode('.', $pluginCode);
if (count($parts) !== 2) {
$this->error('Invalid plugin name, either too many dots or not enough.');
$this->error('Example name: AuthorName.PluginName');
return false;
}
return true;
}
}
================================================
FILE: src/Scaffold/Console/CreateReportWidget.php
================================================
(eg: Acme.Blog)}
{name : The name of the report widget. Eg: TopPages}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new report widget.';
/**
* @var string type of class being generated
*/
protected $typeLabel = 'Report Widget';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$this->makeStub('reportwidget/reportwidget.stub', 'reportwidgets/{{studly_name}}.php');
$this->makeStub('reportwidget/widget.stub', 'reportwidgets/{{lower_name}}/partials/_{{lower_name}}.php');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateSeeder.php
================================================
(eg: Acme.Blog)}
{name : The name of the job class to generate. (eg: PostSeeder)}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new seeder class.';
/**
* @var string typeLabel of class being generated
*/
protected $typeLabel = 'Seeder';
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
if ($this->isAppNamespace()) {
$this->makeStub('seeder/create_app_seeder.stub', 'database/seeders/{{studly_name}}.php');
} else {
$this->makeStub('seeder/create_seeder.stub', 'updates/seeders/{{studly_name}}.php');
}
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/CreateTest.php
================================================
(eg: Acme.Blog)}
{name : The name of the test class to generate. (eg: UserTest)}
{--o|overwrite : Overwrite existing files with generated ones}';
/**
* @var string description of the console command
*/
protected $description = 'Creates a new test class.';
/**
* @var string typeLabel of class being generated
*/
protected $typeLabel = 'Test';
/**
* handle executes the console command
*/
public function handle()
{
if (!str_ends_with($this->argument('name'), 'Test')) {
$this->components->error('Test classes names must end in "Test"');
return;
}
parent::handle();
}
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
if (!file_exists($this->getDestinationPath() . '/phpunit.xml')) {
if ($this->isAppNamespace()) {
$this->makeStub('test/phpunit.app.stub', 'phpunit.xml');
}
else {
$this->makeStub('test/phpunit.plugin.stub', 'phpunit.xml');
}
}
$this->makeStub('test/test.stub', 'tests/{{studly_name}}.php');
}
/**
* prepareVars prepares variables for stubs
*/
protected function prepareVars(): array
{
return [
'name' => $this->argument('name'),
'namespace' => $this->argument('namespace'),
];
}
}
================================================
FILE: src/Scaffold/Console/command/command.stub
================================================
argument('user');
$this->output->writeln("Hello {$username}!");
}
}
================================================
FILE: src/Scaffold/Console/component/component.stub
================================================
'{{title_name}} Component',
'description' => 'No description provided yet...'
];
}
/**
* @link https://docs.octobercms.com/4.x/element/inspector-types.html
*/
public function defineProperties()
{
return [];
}
}
================================================
FILE: src/Scaffold/Console/component/default.stub
================================================
This is the default markup for component {{name}}
You can delete this file if you want
================================================
FILE: src/Scaffold/Console/contentfield/contentfield.stub
================================================
addFormField($this->fieldName, $this->label)->useConfig($this->config)->displayAs('text');
}
/**
* defineListColumn will define how a field is displayed in a list.
*/
public function defineListColumn(ListElement $list, $context = null)
{
$list->defineColumn($this->fieldName, $this->label)->displayAs('switch');
}
/**
* defineFilterScope will define how a field is displayed in a filter.
*/
public function defineFilterScope(FilterElement $filter, $context = null)
{
$filter->defineScope($this->fieldName, $this->label)->displayAs('switch');
}
/**
* extendModelObject will extend the record model.
*/
public function extendModelObject($model)
{
$model->addJsonable($this->fieldName);
}
/**
* extendDatabaseTable adds any required columns to the database.
*/
public function extendDatabaseTable($table)
{
$table->mediumText($this->fieldName)->nullable();
}
}
================================================
FILE: src/Scaffold/Console/controller/_list_toolbar.stub
================================================
================================================
FILE: src/Scaffold/Console/controller/config_form.stub
================================================
# ===================================
# Form Behavior Config
# ===================================
# Record name
name: {{title_singular_name}}
# Model Form Field configuration
form: {{namespace_local}}/models/{{lower_model}}/fields.yaml
# Model Class name
modelClass: {{namespace_php}}\Models\{{studly_model}}
# Default redirect location
defaultRedirect: {{namespace_path}}/{{lower_name}}
{% if design %}
# Form Design
design:
displayMode: {{design}}
{% endif %}
# Create page
create:
title: backend::lang.form.create_title
redirect: {{namespace_path}}/{{lower_name}}/update/:id
redirectClose: {{namespace_path}}/{{lower_name}}
# Update page
update:
title: backend::lang.form.update_title
redirect: {{namespace_path}}/{{lower_name}}
redirectClose: {{namespace_path}}/{{lower_name}}
# Preview page
preview:
title: backend::lang.form.preview_title
================================================
FILE: src/Scaffold/Console/controller/config_list.stub
================================================
# ===================================
# List Behavior Config
# ===================================
# Model List Column configuration
list: {{namespace_local}}/models/{{lower_model}}/columns.yaml
# Model Class name
modelClass: {{namespace_php}}\Models\{{studly_model}}
# List Title
title: Manage {{title_plural_name}}
{% if design == 'popup' %}
# Link each record to popup form design
recordOnClick: popup
{% else %}
# Link URL for each record
recordUrl: {{namespace_path}}/{{lower_name}}/update/:id
{% endif %}
# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records
# Records to display per page
recordsPerPage: 20
# Display page numbers with pagination, disable to improve performance
showPageNumbers: true
# Displays the list column set up button
showSetup: true
# Displays the sorting link on each column
showSorting: true
# Default sorting column
defaultSort:
column: id
direction: asc
# Display checkboxes next to each record
showCheckboxes: true
# Toolbar widget configuration
toolbar:
# Partial for toolbar buttons
buttons: list_toolbar
# Search widget configuration
search:
prompt: backend::lang.list.search_prompt
# List Structure (Optional)
# structure:
# # Allow collapsing of tree items
# showTree: false
#
# # Allow items to be reordered
# showReorder: true
#
# # Maximum reordering depth
# maxDepth: 2
================================================
FILE: src/Scaffold/Console/controller/controller.stub
================================================
= $this->formRenderDesign() ?>
================================================
FILE: src/Scaffold/Console/factory/factory.stub
================================================
*/
public function definition()
{
return [
//
];
}
}
================================================
FILE: src/Scaffold/Console/factory/factory_app.stub
================================================
*/
public function definition()
{
return [
//
];
}
}
================================================
FILE: src/Scaffold/Console/filterwidget/filterwidget.stub
================================================
prepareVars();
return $this->makePartial('{{lower_name}}');
}
public function renderForm()
{
$this->prepareVars();
return $this->makePartial('{{lower_name}}_form');
}
public function prepareVars()
{
$this->vars['scope'] = $this->filterScope;
$this->vars['name'] = $this->getScopeName();
$this->vars['value'] = $this->getLoadValue();
}
public function loadAssets()
{
$this->addCss('css/{{lower_name}}.css');
$this->addJs('js/{{lower_name}}.js');
}
public function getActiveValue()
{
if (post('clearScope')) {
return null;
}
if (!$this->hasPostValue('value')) {
return null;
}
return post('Filter');
}
public function applyScopeToQuery($query)
{
$hasDiscount = $this->filterScope->value;
if ($hasDiscount) {
$query->where('discount', '>', 0);
}
else {
$query->where('discount', 0);
}
}
}
================================================
FILE: src/Scaffold/Console/filterwidget/javascript.stub
================================================
/*
* This is a sample JavaScript file used by {{name}}
*
* You can delete this file if you want
*/
================================================
FILE: src/Scaffold/Console/filterwidget/partial.stub
================================================
= e(trans($scope->label)) ?>1
================================================
FILE: src/Scaffold/Console/filterwidget/partial_form.stub
================================================
================================================
FILE: src/Scaffold/Console/filterwidget/stylesheet.stub
================================================
/*
* This is a sample StyleSheet file used by {{name}}
*
* You can delete this file if you want
*/
================================================
FILE: src/Scaffold/Console/formwidget/formwidget.stub
================================================
prepareVars();
return $this->makePartial('{{lower_name}}');
}
public function prepareVars()
{
$this->vars['name'] = $this->formField->getName();
$this->vars['value'] = $this->getLoadValue();
$this->vars['model'] = $this->model;
}
public function loadAssets()
{
$this->addCss('css/{{lower_name}}.css');
$this->addJs('js/{{lower_name}}.js');
}
public function getSaveValue($value)
{
return $value;
}
}
================================================
FILE: src/Scaffold/Console/formwidget/javascript.stub
================================================
/*
* This is a sample JavaScript file used by {{name}}
*
* You can delete this file if you want
*/
================================================
FILE: src/Scaffold/Console/formwidget/partial.stub
================================================
previewMode): ?>
= $value ?>
================================================
FILE: src/Scaffold/Console/formwidget/stylesheet.stub
================================================
/*
* This is a sample StyleSheet file used by {{name}}
*
* You can delete this file if you want
*/
================================================
FILE: src/Scaffold/Console/job/job.queued.stub
================================================
id();
{% if timestamps %}
$table->timestamps();
{% endif %}{% if softDeletes %}
$table->softDeletes();
{% endif %}
});
}
/**
* down reverses the migration
*/
public function down()
{
Schema::dropIfExists('{{table}}');
}
};
================================================
FILE: src/Scaffold/Console/migration/create_table.stub
================================================
id();
{% if timestamps %}
$table->timestamps();
{% endif %}{% if softDeletes %}
$table->softDeletes();
{% endif %}
});
}
/**
* down reverses the migration
*/
public function down()
{
Schema::dropIfExists('{{table}}');
}
};
================================================
FILE: src/Scaffold/Console/migration/update_app_table.stub
================================================
'{{plugin}}',
'description' => 'No description provided yet...',
'author' => '{{author}}',
'icon' => 'icon-leaf'
];
}
/**
* register method, called when the plugin is first registered.
*/
public function register()
{
//
}
/**
* boot method, called right before the request route.
*/
public function boot()
{
//
}
/**
* registerComponents used by the frontend.
*/
public function registerComponents()
{
return []; // Remove this line to activate
return [
'{{studly_author}}\{{studly_plugin}}\Components\MyComponent' => 'myComponent',
];
}
/**
* registerPermissions used by the backend.
*/
public function registerPermissions()
{
return []; // Remove this line to activate
return [
'{{lower_namespace_code}}.some_permission' => [
'tab' => '{{plugin}}',
'label' => 'Some permission'
],
];
}
/**
* registerNavigation used by the backend.
*/
public function registerNavigation()
{
return []; // Remove this line to activate
return [
'{{lower_plugin}}' => [
'label' => '{{plugin}}',
'url' => Backend::url('{{lower_author}}/{{lower_plugin}}/mycontroller'),
'icon' => 'icon-leaf',
'permissions' => ['{{lower_namespace_code}}.*'],
'order' => 500,
],
];
}
}
================================================
FILE: src/Scaffold/Console/plugin/version.stub
================================================
v1.0.1: First version of {{plugin}}
================================================
FILE: src/Scaffold/Console/reportwidget/reportwidget.stub
================================================
[
'title' => 'Name',
'default' => '{{title_name}} Report Widget',
'type' => 'string',
'validation' => [
'required' => [
'message' => 'The Name field is required'
],
'regex' => [
'message' => 'The Name field can contain only Latin letters.',
'pattern' => '^[a-zA-Z]+$'
]
]
],
];
}
public function render()
{
try {
$this->prepareVars();
}
catch (Exception $ex) {
$this->vars['error'] = $ex->getMessage();
}
return $this->makePartial('{{lower_name}}');
}
public function prepareVars()
{
}
protected function loadAssets()
{
}
}
================================================
FILE: src/Scaffold/Console/reportwidget/widget.stub
================================================
= e($this->property('title')) ?>
This is the default partial content.
= e($error) ?>
================================================
FILE: src/Scaffold/Console/seeder/create_app_seeder.stub
================================================
./tests./tests/browser
================================================
FILE: src/Scaffold/Console/test/phpunit.plugin.stub
================================================
./tests./tests/browser
================================================
FILE: src/Scaffold/Console/test/test.stub
================================================
assertTrue(true);
}
}
================================================
FILE: src/Scaffold/GeneratorCommand.php
================================================
files = new Filesystem;
}
/**
* handle executes the console command
*/
public function handle()
{
$this->vars = $this->processVars($this->prepareVars());
$this->makeStubs();
$this->info($this->type . ' created successfully.');
}
/**
* prepareVars prepares variables for stubs
*/
abstract protected function prepareVars();
/**
* makeStubs makes all stubs
*/
public function makeStubs()
{
$stubs = array_keys($this->stubs);
foreach ($stubs as $stub) {
$this->makeStub($stub);
}
}
/**
* makeStub makes a single stub
*/
public function makeStub(string $stubName)
{
if (!isset($this->stubs[$stubName])) {
return;
}
$sourceFile = $this->getSourcePath() . '/' . $stubName;
$destinationFile = $this->getDestinationPath() . '/' . $this->stubs[$stubName];
$destinationContent = $this->files->get($sourceFile);
// Parse each variable in to the destination content and path
$destinationContent = Twig::parse($destinationContent, $this->vars);
$destinationFile = Twig::parse($destinationFile, $this->vars);
$this->makeDirectory($destinationFile);
// Make sure this file does not already exist
if ($this->files->exists($destinationFile) && !$this->option('force')) {
throw new Exception('Stop everything!!! This file already exists: ' . $destinationFile);
}
$this->files->put($destinationFile, $destinationContent);
}
/**
* makeDirectory builds the directory for the class if necessary
*/
protected function makeDirectory(string $path)
{
if (!$this->files->isDirectory(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0755, true, true);
}
}
/**
* processVars converts all variables to available modifier and case formats
* Syntax is CASE_MODIFIER_KEY, eg: lower_plural_xxx
*/
protected function processVars(array $vars): array
{
$cases = ['upper', 'lower', 'snake', 'studly', 'camel', 'title'];
$modifiers = ['plural', 'singular', 'title'];
foreach ($vars as $key => $var) {
// Apply cases, and cases with modifiers
foreach ($cases as $case) {
$primaryKey = $case . '_' . $key;
$vars[$primaryKey] = $this->modifyString($case, $var);
foreach ($modifiers as $modifier) {
$secondaryKey = $case . '_' . $modifier . '_' . $key;
$vars[$secondaryKey] = $this->modifyString([$modifier, $case], $var);
}
}
// Apply modifiers
foreach ($modifiers as $modifier) {
$primaryKey = $modifier . '_' . $key;
$vars[$primaryKey] = $this->modifyString($modifier, $var);
}
}
return $vars;
}
/**
* modifyString is an internal helper that handles modify a string, with extra logic
*/
protected function modifyString($type, string $string): string
{
if (is_array($type)) {
foreach ($type as $_type) {
$string = $this->modifyString($_type, $string);
}
return $string;
}
if ($type === 'title') {
$string = str_replace('_', ' ', Str::snake($string));
}
return Str::$type($string);
}
/**
* getDestinationPath gets the plugin path from the input
*/
protected function getDestinationPath(): string
{
$plugin = $this->getPluginInput();
$parts = explode('.', $plugin);
$name = array_pop($parts);
$author = array_pop($parts);
return plugins_path(strtolower($author) . '/' . strtolower($name));
}
/**
* getSourcePath gets the source file path
*/
protected function getSourcePath(): string
{
$className = get_class($this);
$class = new ReflectionClass($className);
return dirname($class->getFileName());
}
/**
* getPluginInput gets the desired plugin name from the input
*/
protected function getPluginInput(): string
{
return $this->argument('plugin');
}
/**
* getArguments get the console command arguments
*/
protected function getArguments()
{
return [
['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: RainLab.Blog'],
];
}
/**
* getOptions get the console command options
*/
protected function getOptions()
{
return [
['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'],
];
}
}
================================================
FILE: src/Scaffold/GeneratorCommandBase.php
================================================
files = new Filesystem;
}
/**
* handle executes the console command
*/
public function handle()
{
$this->vars = $this->processVars($this->prepareVars());
$this->makeStubs();
$this->components->info("{$this->typeLabel} created successfully.");
}
/**
* prepareVars prepares variables for stubs
*/
abstract protected function prepareVars();
/**
* makeStubs makes all stubs
*/
abstract public function makeStubs();
/**
* makeStub makes a single stub
*/
public function makeStub(string $stubName, string $outputName)
{
$sourceFile = $this->getSourcePath() . '/' . $stubName;
$destinationFile = $this->getDestinationPath() . '/' . $outputName;
$destinationContent = $this->files->get($sourceFile);
// Parse each variable in to the destination content and path
$destinationContent = Twig::parse($destinationContent, $this->vars);
$destinationFile = Twig::parse($destinationFile, $this->vars);
$this->makeDirectory($destinationFile);
// Make sure this file does not already exist
if ($this->files->exists($destinationFile) && !$this->option('overwrite')) {
throw new Exception('Process halted! This file already exists: ' . $destinationFile);
}
$this->files->put($destinationFile, $destinationContent);
}
/**
* makeDirectory builds the directory for the class if necessary
*/
protected function makeDirectory(string $path)
{
if (!$this->files->isDirectory(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0755, true, true);
}
}
/**
* processVars converts all variables to available modifier and case formats
* Syntax is CASE_MODIFIER_KEY, eg: lower_plural_xxx
*/
protected function processVars(array $vars): array
{
$cases = ['upper', 'lower', 'snake', 'studly', 'camel', 'title'];
$modifiers = ['plural', 'singular', 'title'];
// Namespace modifiers
if (isset($vars['namespace'])) {
$vars += $this->getNamespaceModifiers();
}
// Splice in author and plugin name automatically
[$author, $plugin] = $this->getFormattedNamespace();
$vars += [
'author' => $author,
'plugin' => $plugin,
];
// Process variables
foreach ($vars as $key => $var) {
// Apply cases, and cases with modifiers
foreach ($cases as $case) {
$primaryKey = $case . '_' . $key;
$vars[$primaryKey] = $this->modifyString($case, $var);
foreach ($modifiers as $modifier) {
$secondaryKey = $case . '_' . $modifier . '_' . $key;
$vars[$secondaryKey] = $this->modifyString([$modifier, $case], $var);
}
}
// Apply modifiers
foreach ($modifiers as $modifier) {
$primaryKey = $modifier . '_' . $key;
$vars[$primaryKey] = $this->modifyString($modifier, $var);
}
}
return $vars;
}
/**
* modifyString is an internal helper that handles modify a string, with extra logic
*/
protected function modifyString($type, string $string): string
{
if (is_array($type)) {
foreach ($type as $_type) {
$string = $this->modifyString($_type, $string);
}
return $string;
}
if ($type === 'title') {
$string = str_replace('_', ' ', Str::snake($string));
}
return Str::$type($string);
}
/**
* getNamespaceModifiers
*/
protected function getNamespaceModifiers(): array
{
if ($this->isAppNamespace()) {
return [
'namespace_php' => 'App',
'namespace_code' => 'App',
'namespace_path' => 'app',
'namespace_table' => 'app',
'namespace_local' => '~/app',
];
}
[$author, $plugin] = $this->getFormattedNamespace();
$sAuthor = Str::studly($author);
$sPlugin = Str::studly($plugin);
$lAuthor = mb_strtolower($author);
$lPlugin = mb_strtolower($plugin);
return [
'namespace_php' => "{$sAuthor}\\{$sPlugin}",
'namespace_code' => "{$sAuthor}.{$sPlugin}",
'namespace_path' => "{$lAuthor}/{$lPlugin}",
'namespace_table' => "{$lAuthor}_{$lPlugin}",
'namespace_local' => "$/{$lAuthor}/{$lPlugin}",
];
}
/**
* getNamespaceTable produces a table name (e.g. acme_blog)
*/
protected function getNamespaceTable(): string
{
return $this->getNamespaceModifiers()['namespace_table'];
}
/**
* getDestinationPath gets the app or plugin local path
*/
protected function getDestinationPath(): string
{
if ($this->isAppNamespace()) {
return app_path();
}
return plugins_path($this->getNamespaceModifiers()['namespace_path']);
}
/**
* getSourcePath gets the source file path
*/
protected function getSourcePath(): string
{
$class = new ReflectionClass(static::class);
return dirname($class->getFileName());
}
/**
* getFormattedNamespace returns a tuple of author and plugin name, or app,
* where returned array takes format of [author, name]
*/
protected function getFormattedNamespace(): array
{
$namespace = $this->getNamespaceInput();
if (strpos($namespace, '.') !== false) {
$parts = explode('.', $namespace);
return [$parts[0], $parts[1]];
}
if (strpos($namespace, '\\') !== false) {
$parts = explode('\\', $namespace);
return [$parts[0], $parts[1]];
}
return [$namespace, $namespace];
}
/**
* getNamespaceInput gets the desired plugin name from the input
*/
protected function getNamespaceInput(): string
{
return $this->argument('namespace');
}
/**
* isAppNamespace
*/
protected function isAppNamespace(): bool
{
return mb_strtolower(trim($this->getNamespaceInput())) === 'app';
}
}
================================================
FILE: src/Scaffold/ScaffoldServiceProvider.php
================================================
app->runningInConsole()) {
return;
}
$this->app->singleton('command.create.plugin', CreatePlugin::class);
$this->app->singleton('command.create.model', CreateModel::class);
$this->app->singleton('command.create.migration', CreateMigration::class);
$this->app->singleton('command.create.controller', CreateController::class);
$this->app->singleton('command.create.component', CreateComponent::class);
$this->app->singleton('command.create.formwidget', CreateFormWidget::class);
$this->app->singleton('command.create.reportwidget', CreateReportWidget::class);
$this->app->singleton('command.create.filterwidget', CreateFilterWidget::class);
$this->app->singleton('command.create.contentfield', CreateContentField::class);
$this->app->singleton('command.create.command', CreateCommand::class);
$this->app->singleton('command.create.test', CreateTest::class);
$this->app->singleton('command.create.job', CreateJob::class);
$this->app->singleton('command.create.factory', CreateFactory::class);
$this->app->singleton('command.create.seeder', CreateSeeder::class);
$this->commands('command.create.plugin');
$this->commands('command.create.model');
$this->commands('command.create.migration');
$this->commands('command.create.controller');
$this->commands('command.create.component');
$this->commands('command.create.formwidget');
$this->commands('command.create.reportwidget');
$this->commands('command.create.filterwidget');
$this->commands('command.create.contentfield');
$this->commands('command.create.command');
$this->commands('command.create.test');
$this->commands('command.create.job');
$this->commands('command.create.factory');
$this->commands('command.create.seeder');
}
/**
* provides the returned services.
* @return array
*/
public function provides()
{
return [
'command.create.plugin',
'command.create.model',
'command.create.migration',
'command.create.controller',
'command.create.component',
'command.create.formwidget',
'command.create.reportwidget',
'command.create.filterwidget',
'command.create.contentfield',
'command.create.command',
'command.create.test',
'command.create.job',
'command.create.factory',
'command.create.seeder',
];
}
}
================================================
FILE: src/Support/Arr.php
================================================
$value) {
list($innerKey, $innerValue) = call_user_func($callback, $key, $value);
$results[$innerKey] = $innerValue;
}
return $results;
}
/**
* trans will translate an array, usually for dropdown and checkboxlist options
*/
public static function trans(array $arr): array
{
array_walk_recursive($arr, function(&$value, $key) {
if (is_string($value)) {
$value = Lang::get($value);
}
});
return $arr;
}
}
================================================
FILE: src/Support/Collection.php
================================================
pluck($value, $key)->all();
}
}
================================================
FILE: src/Support/Date.php
================================================
'background-color:#fff; color:#222; line-height:1.2em; font-weight:normal; font:12px Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:100000',
'num' => 'color:#a71d5d',
'const' => 'color:#795da3',
'str' => 'color:#df5000',
'cchr' => 'color:#222',
'note' => 'color:#a71d5d',
'ref' => 'color:#a0a0a0',
'public' => 'color:#795da3',
'protected' => 'color:#795da3',
'private' => 'color:#795da3',
'meta' => 'color:#b729d9',
'key' => 'color:#df5000',
'index' => 'color:#a71d5d',
];
}
================================================
FILE: src/Support/DefaultProviders.php
================================================
providers = $providers ?: [
// October Providers
//
\October\Rain\Foundation\Providers\AppServiceProvider::class,
\October\Rain\Foundation\Providers\DateServiceProvider::class,
\October\Rain\Database\DatabaseServiceProvider::class,
\October\Rain\Halcyon\HalcyonServiceProvider::class,
\October\Rain\Filesystem\FilesystemServiceProvider::class,
\October\Rain\Html\UrlServiceProvider::class,
// October Providers (Deferred)
\October\Rain\Mail\MailServiceProvider::class,
\October\Rain\Html\HtmlServiceProvider::class,
\October\Rain\Flash\FlashServiceProvider::class,
\October\Rain\Parse\ParseServiceProvider::class,
\October\Rain\Assetic\AsseticServiceProvider::class,
\October\Rain\Resize\ResizeServiceProvider::class,
\October\Rain\Validation\ValidationServiceProvider::class,
\October\Rain\Translation\TranslationServiceProvider::class,
\Illuminate\Auth\Passwords\PasswordResetServiceProvider:: class,
// October Console (Deferred)
\October\Rain\Scaffold\ScaffoldServiceProvider::class,
\October\Rain\Foundation\Providers\ConsoleSupportServiceProvider::class,
// Laravel Providers
//
\Illuminate\Broadcasting\BroadcastServiceProvider::class,
\Illuminate\Bus\BusServiceProvider::class,
\Illuminate\Cache\CacheServiceProvider::class,
\Illuminate\Concurrency\ConcurrencyServiceProvider::class,
\Illuminate\Cookie\CookieServiceProvider::class,
\Illuminate\Encryption\EncryptionServiceProvider::class,
\Illuminate\Foundation\Providers\FoundationServiceProvider::class,
\Illuminate\Hashing\HashServiceProvider::class,
\Illuminate\Pagination\PaginationServiceProvider::class,
\Illuminate\Pipeline\PipelineServiceProvider::class,
\Illuminate\Queue\QueueServiceProvider::class,
\Illuminate\Redis\RedisServiceProvider::class,
\Illuminate\Session\SessionServiceProvider::class,
\Illuminate\View\ViewServiceProvider::class,
];
}
}
================================================
FILE: src/Support/Facade.php
================================================
bound($name) &&
($instance = static::getFacadeInstance()) !== null
) {
static::$app->instance($name, $instance);
}
return parent::resolveFacadeInstance($name);
}
/**
* getFacadeInstance if the accessor is not found via getFacadeAccessor,
* use this instance as a fallback.
* @return mixed
*/
protected static function getFacadeInstance()
{
return null;
}
/**
* defaultAliases gets the application default aliases.
* @return \Illuminate\Support\Collection
*/
public static function defaultAliases()
{
return parent::defaultAliases()->merge([
'Model' => \October\Rain\Database\Model::class,
'Event' => \October\Rain\Support\Facades\Event::class,
'Mail' => \October\Rain\Support\Facades\Mail::class,
'File' => \October\Rain\Support\Facades\File::class,
'Config' => \October\Rain\Support\Facades\Config::class,
'Seeder' => \October\Rain\Database\Updates\Seeder::class,
'Input' => \October\Rain\Support\Facades\Input::class,
'Str' => \October\Rain\Support\Facades\Str::class,
]);
}
}
================================================
FILE: src/Support/Facades/Auth.php
================================================
input($key, $default);
}
/**
* getFacadeAccessor returns the registered name of the component
* @return string
*/
protected static function getFacadeAccessor()
{
return 'request';
}
}
================================================
FILE: src/Support/Facades/Mail.php
================================================
getModule(func_get_args());
if (!$module) {
return;
}
$modulePath = base_path('modules/' . $module);
// Register configuration path
$configPath = $modulePath . '/config';
if (!$this->app->configurationIsCached() && is_dir($configPath)) {
$this->loadConfigFrom($configPath, $module);
}
// Register view path
$viewsPath = $modulePath . '/views';
if (is_dir($viewsPath)) {
$this->loadViewsFrom($viewsPath, $module);
}
// Load translator
$this->loadTranslationsFrom($modulePath . '/lang', $module);
if ($this->app->runningInBackend()) {
$this->loadJsonTranslationsFrom($modulePath . '/lang');
}
// Add routes, if available
$routesFile = $modulePath . '/routes.php';
if (!$this->app->routesAreCached() && file_exists($routesFile)) {
$this->loadRoutesFrom($routesFile);
}
}
/**
* boot bootstraps the application events
*/
public function boot()
{
$module = $this->getModule(func_get_args());
if (!$module) {
return;
}
// Reserved for boot logic
}
/**
* @inheritDoc
*/
public function registerMarkupTags()
{
return [];
}
/**
* @inheritDoc
*/
public function registerComponents()
{
return [];
}
/**
* @inheritDoc
*/
public function registerPageSnippets()
{
return [];
}
/**
* @inheritDoc
*/
public function registerContentFields()
{
return [];
}
/**
* @inheritDoc
*/
public function registerNavigation()
{
return [];
}
/**
* @inheritDoc
*/
public function registerPermissions()
{
return [];
}
/**
* @inheritDoc
*/
public function registerSettings()
{
return [];
}
/**
* @inheritDoc
*/
public function registerSchedule($schedule)
{
}
/**
* @inheritDoc
*/
public function registerReportWidgets()
{
return [];
}
/**
* @inheritDoc
*/
public function registerFormWidgets()
{
return [];
}
/**
* @inheritDoc
*/
public function registerFilterWidgets()
{
return [];
}
/**
* @inheritDoc
*/
public function registerListColumnTypes()
{
return [];
}
/**
* @inheritDoc
*/
public function registerMailLayouts()
{
return [];
}
/**
* @inheritDoc
*/
public function registerMailTemplates()
{
return [];
}
/**
* @inheritDoc
*/
public function registerMailPartials()
{
return [];
}
/**
* getModule gets the module name from method args
*/
protected function getModule($args)
{
return isset($args[0]) && is_string($args[0]) ? $args[0] : null;
}
/**
* discoverConsoleCommands automatically finds and registers console
* commands from the module's console directory
*/
protected function discoverConsoleCommands(string $module): void
{
Artisan::starting(function ($artisan) use ($module) {
$consolePath = base_path('modules/' . $module . '/console');
if (!is_dir($consolePath)) {
return;
}
$namespace = ucfirst($module) . '\\Console\\';
$commands = [];
foreach (glob($consolePath . '/*.php') as $file) {
$className = $namespace . basename($file, '.php');
if (!class_exists($className)) {
continue;
}
$reflection = new \ReflectionClass($className);
if (
$reflection->isSubclassOf(\Illuminate\Console\Command::class) &&
!$reflection->isAbstract()
) {
$commands[] = $className;
}
}
$artisan->resolveCommands($commands);
});
}
/**
* registerConsoleCommand registers a new console (artisan) command
*/
protected function registerConsoleCommand(string $key, string $class)
{
$key = 'command.'.$key;
$this->app->singleton($key, function ($app) use ($class) {
return $this->app->make($class);
});
$this->commands($key);
}
/**
* loadConfigFrom registers a config file namespace
* @param string $path
* @param string $namespace
*/
protected function loadConfigFrom($path, $namespace)
{
$this->app['config']->package($namespace, $path);
}
}
================================================
FILE: src/Support/README.md
================================================
## Rain Support
The October Rain Support contains common classes relevant to supporting the other October Rain libraries. It adds the following features:
### Scaffolding
See the Scaffolding Commands section of the [Console documentation](https://octobercms.com/docs/console/commands).
### A true Singleton trait
A *true singleton* is a class that can ever only have a single instance, no matter what. Use it in your classes like this:
class MyClass
{
use \October\Rain\Support\Traits\Singleton;
}
$class = MyClass::instance();
### Global helpers
**input()**
Similar to `Input::get()` this returns an input parameter or the default value. However it supports HTML Array names. Booleans are also converted from strings.
$value = input('value', 'not found');
$name = input('contact[name]');
$city = input('contact[location][city]');
### Event emitter
Adds event related features to any class.
**Attach to a class**
class MyClass
{
use October\Rain\Support\Traits\Emitter;
}
**Bind to an event**
$myObject = new MyClass;
$myObject->bindEvent('cook.bacon', function(){
echo 'Bacon is ready';
});
**Trigger an event**
// Outputs: Bacon is ready
$myObject->fireEvent('cook.bacon');
**Bind to an event only once**
$myObject = new MyClass;
$myObject->bindEvent('cook.soup', function(){
echo 'Soup is ready. Want more? NO SOUP FOR YOU!';
}, true);
**Bind an event to other object method**
$myObject->bindEvent('cook.eggs', [$anotherObject, 'methodToCookEggs']);
**Unbind an event**
$myObject->unbindEvent('cook.bacon');
$myObject->unbindEvent(['cook.bacon', 'cook.eggs']);
================================================
FILE: src/Support/SafeCollection.php
================================================
app->beforeResolving($name, $callback);
if ($this->app->resolved($name)) {
$callback($this->app->make($name), $this->app);
}
}
/**
* Get the default providers for a Laravel application.
*
* @return \October\Rain\Support\DefaultProviders
*/
public static function defaultProviders()
{
return new DefaultProviders;
}
}
================================================
FILE: src/Support/Singleton.php
================================================
init();
}
/**
* instance creates a new instance of this singleton
*/
final public static function instance()
{
$accessor = static::getSingletonAccessor();
if (!App::bound($accessor)) {
App::singleton($accessor, function () {
return static::getSingletonInstance();
});
}
return App::make($accessor);
}
/**
* getSingletonAccessor should return a meaningful IoC container code.
* Eg: backend.helper
*/
protected static function getSingletonAccessor()
{
return get_called_class();
}
/**
* getSingletonInstance returns the final instance of this singleton
*/
final public static function getSingletonInstance()
{
return new static;
}
/**
* init the singleton free from constructor parameters
*/
protected function init()
{
}
/**
* __clone
* @ignore
*/
public function __clone()
{
trigger_error('Cloning '.__CLASS__.' is not allowed.', E_USER_ERROR);
}
/**
* __wakeup
* @ignore
*/
public function __wakeup()
{
trigger_error('Unserializing '.__CLASS__.' is not allowed.', E_USER_ERROR);
}
}
================================================
FILE: src/Support/Str.php
================================================
$dictionary
* @return string
*/
public static function slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at'])
{
$title = str_replace(['\\', '/'], ' ', (string) $title);
return parent::slug($title, $separator, $language, $dictionary);
}
/**
* ascii applies transliterate when the language is not found
*
* @param string $value
* @param string $language
* @return string
*/
public static function ascii($value, $language = 'en')
{
return ASCII::to_ascii((string) $value, $language, true, false, true);
}
/**
* ordinal converts number to its ordinal English form
*
* This method converts 13 to 13th, 2 to 2nd ...
*
* @param integer $number Number to get its ordinal value
* @return string Ordinal representation of given string.
*/
public static function ordinal($number)
{
if (in_array($number % 100, range(11, 13))) {
return $number.'th';
}
switch ($number % 10) {
case 1:
return $number.'st';
case 2:
return $number.'nd';
case 3:
return $number.'rd';
default:
return $number.'th';
}
}
/**
* shortNumber converts a large number to its abbreviated form. This
* method converts 1000 to 1K, 1500000 to 1.5M, etc.
*/
public static function shortNumber($number, $precision = 1): string
{
if ($number < 1000) {
return (string) $number;
}
$units = ['', 'K', 'M', 'B', 'T'];
$power = (int) floor(log($number, 1000));
$value = $number / pow(1000, $power);
// Remove trailing .0 if any
$formattedValue = round($value, $precision);
if ($precision > 0) {
$formattedValue = rtrim(rtrim(number_format($formattedValue, $precision, '.', ''), '0'), '.');
}
return $formattedValue . $units[$power];
}
/**
* normalizeEol converts line breaks to a standard \r\n pattern
*/
public static function normalizeEol($string)
{
return preg_replace('~\R~u', "\r\n", $string);
}
/**
* normalizeClassName removes the starting slash from a class namespace \
*/
public static function normalizeClassName($name)
{
if (is_object($name)) {
$name = get_class($name);
}
return ltrim($name, '\\');
}
/**
* getClassId generates a class ID from either an object or a string of the class name
*/
public static function getClassId($name)
{
if (is_object($name)) {
$name = get_class($name);
}
$name = ltrim($name, '\\');
$name = str_replace('\\', '_', $name);
return strtolower($name);
}
/**
* getClassNamespace returns a class namespace
*/
public static function getClassNamespace($name)
{
$name = static::normalizeClassName($name);
return substr($name, 0, strrpos($name, "\\"));
}
/**
* getPrecedingSymbols checks if $string begins with any number of consecutive symbols,
* returns the number, otherwise returns 0
*/
public static function getPrecedingSymbols(string $string, string $symbol): int
{
return strlen($string) - strlen(ltrim($string, $symbol));
}
/**
* limitMiddle limits the length of a string by removing characters from the middle
*
* @param string $value
* @param int $limit
* @param string $marker
* @return string
*/
public static function limitMiddle($value, $limit = 100, $marker = '...')
{
if (mb_strwidth($value, 'UTF-8') <= $limit) {
return $value;
}
if ($limit > 3) {
$limit -= 3;
}
$limitStart = floor($limit / 2);
$limitEnd = $limit - $limitStart;
$valueStart = rtrim(mb_strimwidth($value, 0, $limitStart, '', 'UTF-8'));
$valueEnd = ltrim(mb_strimwidth($value, $limitEnd * -1, $limitEnd, '', 'UTF-8'));
return $valueStart . $marker . $valueEnd;
}
}
================================================
FILE: src/Support/Traits/Emitter.php
================================================
emitterEventCollection[$event][$priority][] = $callback;
unset($this->emitterEventSorted[$event]);
}
/**
* bindEventOnce creates a new event binding that fires once only
* @return void
*/
public function bindEventOnce($event, $callback, $priority = 0)
{
$this->emitterSingleEventCollection[$event][$priority][] = $callback;
unset($this->emitterEventSorted[$event]);
}
/**
* unbindEvent destroys an event binding
* @return void
*/
public function unbindEvent($event = null)
{
if (is_array($event)) {
foreach ($event as $_event) {
$this->unbindEvent($_event);
}
return;
}
if ($event === null) {
unset($this->emitterSingleEventCollection);
unset($this->emitterEventCollection);
unset($this->emitterEventSorted);
return;
}
unset($this->emitterSingleEventCollection[$event]);
unset($this->emitterEventCollection[$event]);
unset($this->emitterEventSorted[$event]);
}
/**
* fireEvent and call the listeners
* @param string $event Event name
* @param array $params Event parameters
* @param boolean $halt Halt after first non-null result
* @return array Collection of event results / Or single result (if halted)
*/
public function fireEvent($event, $params = [], $halt = false)
{
if (!is_array($params)) {
$params = [$params];
}
// Micro optimization
if (
!isset($this->emitterEventCollection[$event]) &&
!isset($this->emitterSingleEventCollection[$event])
) {
return $halt ? null : [];
}
if (!isset($this->emitterEventSorted[$event])) {
$this->emitterEventSorted[$event] = $this->emitterEventSortEvents($event);
}
$result = [];
foreach ($this->emitterEventSorted[$event] as $callback) {
$response = $callback(...$params);
if (!is_null($response) && $halt) {
return $response;
}
if ($response === false) {
break;
}
if (!is_null($response)) {
$result[] = $response;
}
}
if (isset($this->emitterSingleEventCollection[$event])) {
unset($this->emitterSingleEventCollection[$event]);
unset($this->emitterEventSorted[$event]);
}
return $halt ? null : $result;
}
/**
* emitterEventSortEvents sorts the listeners for a given event by priority
*/
protected function emitterEventSortEvents(string $eventName, array $combined = []): array
{
if (isset($this->emitterEventCollection[$eventName])) {
foreach ($this->emitterEventCollection[$eventName] as $priority => $callbacks) {
$combined[$priority] = array_merge($combined[$priority] ?? [], $callbacks);
}
}
if (isset($this->emitterSingleEventCollection[$eventName])) {
foreach ($this->emitterSingleEventCollection[$eventName] as $priority => $callbacks) {
$combined[$priority] = array_merge($combined[$priority] ?? [], $callbacks);
}
}
krsort($combined);
return call_user_func_array('array_merge', $combined);
}
}
================================================
FILE: src/Support/Traits/KeyParser.php
================================================
keyParserCache[$key] = $parsed;
}
/**
* parseKey into namespace, group, and item
*/
public function parseKey(string $key): array
{
// If we've already parsed the given key, we'll return the cached version we
// already have, as this will save us some processing. We cache off every
// key we parse so we can quickly return it on all subsequent requests.
if (isset($this->keyParserCache[$key])) {
return $this->keyParserCache[$key];
}
$segments = explode('.', $key);
// If the key does not contain a double colon, it means the key is not in a
// namespace, and is just a regular configuration item. Namespaces are a
// tool for organizing configuration items for things such as modules.
if (strpos($key, '::') === false) {
$parsed = $this->keyParserParseBasicSegments($segments);
}
else {
$parsed = $this->keyParserParseSegments($key);
}
// Once we have the parsed array of this key's elements, such as its groups
// and namespace, we will cache each array inside a simple list that has
// the key and the parsed array for quick look-ups for later requests.
return $this->keyParserCache[$key] = $parsed;
}
/**
* keyParserParseBasicSegments as an array
*/
protected function keyParserParseBasicSegments(array $segments): array
{
// The first segment in a basic array will always be the group, so we can go
// ahead and grab that segment. If there is only one total segment we are
// just pulling an entire group out of the array and not a single item.
$group = $segments[0];
if (count($segments) === 1) {
return [null, $group, null];
}
// If there is more than one segment in this group, it means we are pulling
// a specific item out of a groups and will need to return the item name
// as well as the group so we know which item to pull from the arrays.
$item = implode('.', array_slice($segments, 1));
return [null, $group, $item];
}
/**
* keyParserParseSegments from a string
*/
protected function keyParserParseSegments(string $key): array
{
list($namespace, $item) = explode('::', $key);
// First we'll just explode the first segment to get the namespace and group
// since the item should be in the remaining segments. Once we have these
// two pieces of data we can proceed with parsing out the item's value.
$itemSegments = explode('.', $item);
$groupAndItem = array_slice($this->keyParserParseBasicSegments($itemSegments), 1);
return array_merge([$namespace], $groupAndItem);
}
}
================================================
FILE: src/Support/Traits/Singleton.php
================================================
init();
}
/**
* init the singleton free from constructor parameters
*/
protected function init()
{
}
/**
* __clone
* @ignore
*/
public function __clone()
{
trigger_error('Cloning '.__CLASS__.' is not allowed.', E_USER_ERROR);
}
/**
* __wakeup
* @ignore
*/
public function __wakeup()
{
trigger_error('Unserializing '.__CLASS__.' is not allowed.', E_USER_ERROR);
}
}
================================================
FILE: src/Translation/FileLoader.php
================================================
= 10
*/
protected $path;
/**
* @var array paths are used by default for the loader.
*
* @todo Can be removed if Laravel >= 10
*/
protected $paths;
/**
* loadNamespaceOverrides loads a local namespaced translation group for overrides
*/
protected function loadNamespaceOverrides(array $lines, $locale, $group, $namespace)
{
$paths = (array) $this->path ?: $this->paths;
return collect($paths)
->reduce(function ($output, $path) use ($lines, $locale, $group, $namespace) {
$namespace = str_replace('.', '/', $namespace);
$file = "{$path}/{$namespace}/{$locale}/{$group}.php";
if ($this->files->exists($file)) {
return array_replace_recursive($lines, $this->files->getRequire($file));
}
return $lines;
}, []);
}
}
================================================
FILE: src/Translation/README.md
================================================
# Translation
An extension of illuminate\translation.
Modules and plugins can have localization files in the /lang directory. Plugin and module localization files are registered automatically.
## Accessing localization strings
```php
// Get a localization string from the CMS module
echo Lang::get('cms::errors.page.not_found');
// Get a localization string from the october/blog plugin.
echo Lang::get('october.blog::messages.post.added');
```
## Overriding localization strings
System users can override localization strings without altering the modules' and plugins' files. This is done by adding localization files to the app/lang directory. To override a plugin's localization:
```
app
lang
en
vendorname
pluginname
file.php
```
Example: lang/en/october/blog/errors.php
To override a module's localization:
```
app
lang
en
modulename
file.php
```
Example: lang/en/cms/errors.php
================================================
FILE: src/Translation/TranslationServiceProvider.php
================================================
registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
/**
* registerLoader registers the line loader
*/
protected function registerLoader()
{
$this->app->singleton('translation.loader', function ($app) {
return new FileLoader($app['files'], $app['path.lang']);
});
}
/**
* provides gets the services provided by the provider
*/
public function provides()
{
return ['translator', 'translation.loader'];
}
}
================================================
FILE: src/Translation/Translator.php
================================================
getValidationSpecific($key, $replace, $locale)) {
return $line;
}
// This is debug code to determine if language keys are
// migrated to JSON or translated in the first place
//
// $locale = $locale ?: $this->locale;
// $val = parent::get($key, $replace, $locale, $fallback);
// if (!isset($this->loaded['*']['*'][$locale][$key])) {
// return is_string($val) ? '→'.$val.'←' : $val;
// }
// return $val;
// Begin CC
$locale = $locale ?: $this->locale;
$this->load('*', '*', $locale);
$line = $this->loaded['*']['*'][$locale][$key] ?? null;
// Laravel notes that with JSON translations, there is no usage of a fallback language.
// The key is the translation. Here we extend the technology to add fallback support.
if ($fallback && $line === null && $this->fallback !== null) {
$this->load('*', '*', $this->fallback);
$line = $this->loaded['*']['*'][$this->fallback][$key] ?? null;
}
if (!isset($line)) {
[$namespace, $group, $item] = $this->parseKey($key);
$locales = $fallback ? $this->localeArray($locale) : [$locale];
foreach ($locales as $locale) {
if (!is_null($line = $this->getLine(
$namespace, $group, $locale, $item, $replace
))) {
return $line;
}
}
}
return $this->makeReplacements($line ?: $key, $replace);
}
/**
* set a given language key value.
*
* @param array|string $key
* @param mixed $value
* @param string|null $locale
* @return void
*/
public function set($key, $value = null, $locale = null)
{
if (is_array($key)) {
foreach ($key as $innerKey => $innerValue) {
$this->set($innerKey, $innerValue, $locale);
}
}
else {
$locale = $locale ?: $this->locale;
$this->loaded['*']['*'][$locale][$key] = $value;
}
}
/**
* getValidationSpecific checks the system namespace by default for "validation" keys
*/
protected function getValidationSpecific($key, $replace, $locale)
{
if (
str_starts_with($key, 'validation.') &&
!str_starts_with($key, 'validation.custom.') &&
!str_starts_with($key, 'validation.attributes.')
) {
$nativeKey = 'system::'.$key;
$line = $this->get($nativeKey, $replace, $locale);
if ($line !== $nativeKey) {
return $line;
}
}
return null;
}
/**
* trans returns the translation for a given key
*
* @param array|string $id
* @param array $parameters
* @param string $locale
* @return string
*/
public function trans($id, array $parameters = [], $locale = null)
{
return $this->get($id, $parameters, $locale);
}
/**
* transChoice gets a translation according to an integer value
*
* @param string $id
* @param int $number
* @param array $parameters
* @param string $locale
* @return string
*/
public function transChoice($id, $number, array $parameters = [], $locale = null)
{
return $this->choice($id, $number, $parameters, $locale);
}
/**
* localeArray gets the array of locales to be checked
*
* @param string|null $locale
* @return array
*/
protected function localeArray($locale)
{
return array_filter([$locale ?: $this->locale, $this->fallback, static::CORE_LOCALE]);
}
}
================================================
FILE: src/Validation/Concerns/FormatsMessages.php
================================================
replacePlaceholderInString($attribute);
$inlineMessage = $this->getInlineMessage($attribute, $rule);
// First we will retrieve the custom message for the validation rule if one
// exists. If a custom validation message is being used we'll return the
// custom message, otherwise we'll keep searching for a valid message.
if (!is_null($inlineMessage)) {
return $inlineMessage;
}
$lowerRule = Str::snake($rule);
$customKey = "validation.custom.{$attribute}.{$lowerRule}";
$customMessage = $this->getCustomMessageFromTranslator(
in_array($rule, $this->sizeRules)
? [$customKey.".{$this->getAttributeType($attribute)}", $customKey]
: $customKey
);
// First we check for a custom defined validation message for the attribute
// and rule. This allows the developer to specify specific messages for
// only some attributes and rules that need to get specially formed.
if ($customMessage !== $customKey) {
return $customMessage;
}
// Modification: Apply fallback message from extension class, if one exists.
if ($this->hasExtensionMethod($lowerRule, 'message')) {
return $this->callExtensionMethod($lowerRule, 'message');
}
// If the rule being validated is a "size" rule, we will need to gather the
// specific error message for the type of attribute being validated such
// as a number, file or string which all have different message types.
if (in_array($rule, $this->sizeRules)) {
return $this->getSizeMessage($attributeWithPlaceholders, $rule);
}
// Finally, if no developer specified messages have been set, and no other
// special messages apply for this rule, we will just pull the default
// messages out of the translator service for this validation rule.
$key = "validation.{$lowerRule}";
if ($key != ($value = $this->translator->get($key))) {
return $value;
}
return $this->getFromLocalArray(
$attribute,
$lowerRule,
$this->fallbackMessages
) ?: $key;
}
/**
* makeReplacements replace all error message place-holders with actual values.
* @param string $message
* @param string $attribute
* @param string $rule
* @param array $parameters
* @return string
*/
public function makeReplacements($message, $attribute, $rule, $parameters)
{
$message = $this->replaceAttributePlaceholder(
$message, $this->getDisplayableAttribute($attribute)
);
$lowerRule = Str::snake($rule);
$message = $this->replaceInputPlaceholder($message, $attribute);
$message = $this->replaceIndexPlaceholder($message, $attribute);
$message = $this->replacePositionPlaceholder($message, $attribute);
if (isset($this->replacers[$lowerRule])) {
return $this->callReplacer($message, $attribute, $lowerRule, $parameters, $this);
}
elseif (method_exists($this, $replacer = "replace{$rule}")) {
return $this->$replacer($message, $attribute, $rule, $parameters);
}
// Modification: Apply fallback replacer from extension class, if one exists.
if ($this->hasExtensionMethod($lowerRule, 'replace')) {
return $this->callExtensionMethod($lowerRule, 'replace', [$message, $attribute, $lowerRule, $parameters]);
}
return $message;
}
/**
* hasExtensionMethod determines if an extended rule has a given method.
*/
protected function hasExtensionMethod(string $rule, string $methodName): bool
{
if (!isset($this->extensions[$rule]) || !is_string($this->extensions[$rule])) {
return false;
}
[$class, $method] = Str::parseCallback($this->extensions[$rule]);
if (!method_exists($class, $methodName)) {
return false;
}
return true;
}
/**
* callExtensionMethod calls a method for an extended rule and returns the result as a string.
*/
protected function callExtensionMethod(string $rule, string $methodName, array $args = []): string
{
[$class, $method] = Str::parseCallback($this->extensions[$rule]);
return (string) call_user_func_array([$this->container->make($class), $methodName], $args);
}
}
================================================
FILE: src/Validation/Factory.php
================================================
resolver)) {
return new Validator($this->translator, $data, $rules, $messages, $customAttributes);
}
return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
}
}
================================================
FILE: src/Validation/ValidationServiceProvider.php
================================================
app->singleton('validator', function ($app) {
$validator = new Factory($app['translator'], $app);
if (isset($app['db'], $app['validation.presence'])) {
$validator->setPresenceVerifier($app['validation.presence']);
}
// Replacers for custom rules in Validator class
$validator->replacer('unique_site', function ($message, $attribute, $rule, $parameters) {
return __('validation.unique', ['attribute' => $attribute]);
});
return $validator;
});
}
}
================================================
FILE: src/Validation/Validator.php
================================================
requireParameterCount(1, $parameters, 'unique_site');
[$connection, $table, $idColumn] = $this->parseTable($parameters[0]);
// The second parameter position holds the name of the column that needs to
// be verified as unique. If this parameter isn't specified we will just
// assume that this column to be verified shares the attribute's name.
$column = $this->getQueryColumn($parameters, $attribute);
$id = null;
if (isset($parameters[2])) {
[$idColumn, $id] = $this->getUniqueIds($idColumn, $parameters);
if (!is_null($id)) {
$id = stripslashes($id);
}
}
// The presence verifier is responsible for counting rows within this store
// mechanism which might be a relational database or any other permanent
// data store like Redis, etc. We will use it to determine uniqueness.
$verifier = $this->getPresenceVerifier($connection);
$extra = $this->getUniqueExtra($parameters);
if ($this->currentRule instanceof Unique) {
$extra = array_merge($extra, $this->currentRule->queryCallbacks());
}
// Add the site extra
$extra['site_id'] = Site::getSiteIdFromContext();
return $verifier->getCount(
$table, $column, $value, $id, $idColumn, $extra
) == 0;
}
}
================================================
FILE: tests/Assetic/MockAsset.php
================================================
content = $content;
}
public function ensureFilter(FilterInterface $filter): void
{
}
public function getFilters(): array
{
return [];
}
public function clearFilters(): void
{
}
public function load(?FilterInterface $additionalFilter = null): void
{
}
public function dump(?FilterInterface $additionalFilter = null): string
{
return $this->content ?? '';
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): void
{
$this->content = $content;
}
public function getSourceRoot(): ?string
{
return null;
}
public function getSourcePath(): ?string
{
return null;
}
public function getSourceDirectory(): ?string
{
return null;
}
public function getTargetPath(): ?string
{
return null;
}
public function setTargetPath(?string $targetPath): void
{
}
public function getLastModified(): ?int
{
return null;
}
public function getVars(): array
{
return [];
}
public function setValues(array $values): void
{
}
public function getValues(): array
{
return [];
}
}
================================================
FILE: tests/Assetic/StylesheetMinifyTest.php
================================================
filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testEmptyClassPreserve()
{
$input = ''.
'.view { /*
* Text
*/
/*
* Links
*/
/*
* Table
*/
/*
* Table cell
*/
/*
* Images
*/ }';
$output = '.view{}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testEmptyCommentPreserve()
{
$input = ''.
'
body { background: blue; }
/**/
.view { color: red; }';
$output = 'body{background:blue}/**/.view{color:red}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testSpecialCommentPreservation()
{
$input = 'body {/*! Keep me */}';
$output = 'body{/*! Keep me */}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testCommentRemoval()
{
$input = 'body{/* First comment */} /* Second comment */';
$output = 'body{}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testCommentPreservationInVar()
{
$input = '--ring-inset: var(--empty, /*!*/ /*!*/);';
$output = '--ring-inset:var(--empty,/*!*/ /*!*/);';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testMinifyPreservationInVar()
{
$input = ''.
'select:focus {
--ring-inset: var(--empty, /*!*/ /*!*/);
--ring-offset-width: 0px;
--ring-offset-color: #fff;
border-color: red;
}';
$output = 'select:focus{--ring-inset:var(--empty,/*!*/ /*!*/);--ring-offset-width:0px;--ring-offset-color:#fff;border-color:red}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testUnitPreservationInVar()
{
$input = '--offset-width: 0px';
$output = '--offset-width:0px';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testAttributeSelectorsWithLess()
{
$input = ''.
'[class^="icon-"]:before,
[class*=" icon-"]:before {
speak: none;
}
/* makes the font 33% larger relative to the icon container */
.icon-large:before {
speak: initial;
}';
$output = '[class^="icon-"]:before,[class*=" icon-"]:before{speak:none}.icon-large:before{speak:initial}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
public function testSourceMappingUrlWithSpecialComment()
{
$input = ''.
'/*! keep me */*,:after,:before { opacity: 1; }body {background: purple;}
/*# sourceMappingUrl*/';
$output = '/*! keep me */*,:after,:before{opacity:1}body{background:purple}';
$mockAsset = new MockAsset($input);
$result = new StylesheetMinify();
$result->filterDump($mockAsset);
$this->assertEquals($output, $mockAsset->getContent());
}
}
================================================
FILE: tests/Benchmark/Database/DatabaseBench.php
================================================
dongleMysql = new Dongle('mysql');
$this->dongleSqlite = new Dongle('sqlite');
$this->donglePgsql = new Dongle('pgsql');
}
/**
* @Subject
*/
public function benchParseConcatMysql()
{
$this->dongleMysql->parseConcat("SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users");
}
/**
* @Subject
*/
public function benchParseConcatSqlite()
{
$this->dongleSqlite->parseConcat("SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users");
}
/**
* @Subject
*/
public function benchParseConcatPgsql()
{
$this->donglePgsql->parseConcat("SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users");
}
/**
* @Subject
*/
public function benchParseGroupConcatMysql()
{
$this->dongleMysql->parseGroupConcat("SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags");
}
/**
* @Subject
*/
public function benchParseGroupConcatSqlite()
{
$this->dongleSqlite->parseGroupConcat("SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags");
}
/**
* @Subject
*/
public function benchParseGroupConcatPgsql()
{
$this->donglePgsql->parseGroupConcat("SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags");
}
/**
* @Subject
*/
public function benchParseIfNullMysql()
{
$this->dongleMysql->parseIfNull("SELECT IFNULL(nickname, username) FROM users");
}
/**
* @Subject
*/
public function benchParseIfNullPgsql()
{
$this->donglePgsql->parseIfNull("SELECT IFNULL(nickname, username) FROM users");
}
/**
* @Subject
*/
public function benchParseBooleanExpressionMysql()
{
$this->dongleMysql->parseBooleanExpression("SELECT * FROM users WHERE is_active = true AND is_deleted = false");
}
/**
* @Subject
*/
public function benchParseBooleanExpressionSqlite()
{
$this->dongleSqlite->parseBooleanExpression("SELECT * FROM users WHERE is_active = true AND is_deleted = false");
}
/**
* @Subject
*/
public function benchParseFullQuerySqlite()
{
$this->dongleSqlite->parse("SELECT CONCAT(first_name, ' ', last_name), IFNULL(nickname, 'N/A') FROM users WHERE is_active = true");
}
/**
* @Subject
*/
public function benchParseFullQueryPgsql()
{
$this->donglePgsql->parse("SELECT CONCAT(first_name, ' ', last_name), IFNULL(nickname, 'N/A') FROM users WHERE is_active = true");
}
}
================================================
FILE: tests/Benchmark/GeneralBench.php
================================================
parse('**Hello**');
}
/**
* @Subject
*/
public function benchB()
{
\Str::markdown('**Hello**');
}
}
================================================
FILE: tests/Benchmark/Parse/ParseBench.php
================================================
ini = new Ini;
$this->bracket = new Bracket;
$this->parsedown = new ParsedownExtra;
// INI content for parsing
$this->iniContent = <<iniArray = [
'title' => 'My Application',
'debug' => true,
'version' => 1.5,
'database' => [
'host' => 'localhost',
'port' => 3306,
'name' => 'october_db'
],
'cache' => [
'driver' => 'redis',
'prefix' => 'app_',
'ttl' => 3600
]
];
// Bracket template
$this->bracketTemplate = <<bracketData = [
'name' => 'John Doe',
'site' => 'October CMS',
'email' => 'john@example.com',
'role' => 'Administrator',
'items' => [
['title' => 'Posts', 'value' => '42'],
['title' => 'Comments', 'value' => '128'],
['title' => 'Likes', 'value' => '256']
]
];
// Simple markdown
$this->markdownSimple = '**Hello** _world_!';
// Complex markdown
$this->markdownComplex = << A blockquote for emphasis
[Visit our site](https://octobercms.com)
MD;
}
//
// INI Benchmarks
//
/**
* @Subject
*/
public function benchIniParse()
{
$this->ini->parse($this->iniContent);
}
/**
* @Subject
*/
public function benchIniRender()
{
$this->ini->render($this->iniArray);
}
//
// Bracket Benchmarks
//
/**
* @Subject
*/
public function benchBracketParseSimple()
{
$this->bracket->parseString('Hello {name}!', ['name' => 'World']);
}
/**
* @Subject
*/
public function benchBracketParseWithLoop()
{
$this->bracket->parseString($this->bracketTemplate, $this->bracketData);
}
//
// Markdown Benchmarks (using ParsedownExtra directly to avoid facade dependency)
//
/**
* @Subject
*/
public function benchMarkdownParseSimple()
{
$this->parsedown->text($this->markdownSimple);
}
/**
* @Subject
*/
public function benchMarkdownParseComplex()
{
$this->parsedown->text($this->markdownComplex);
}
/**
* @Subject
*/
public function benchMarkdownParseLine()
{
$this->parsedown->line($this->markdownSimple);
}
}
================================================
FILE: tests/Benchmark/Router/RouterBench.php
================================================
fixtures as $index => $rule) {
$routes['pad1'.$index] = '/pad1/'.$rule;
$routes['pad2'.$index] = '/pad2/'.$rule;
$routes['pad3'.$index] = '/pad3/'.$rule;
$routes['pad3'.$index] = '/pad4/'.$rule;
$routes['pad3'.$index] = '/pad5/'.$rule;
}
// Final target at end (120 routes)
foreach ($this->fixtures as $index => $rule) {
$routes['rule'.$index] = $rule;
}
// Register with router
foreach ($routes as $name => $rule) {
$router->route($name, $rule);
}
$this->routes = $routes;
$this->routesCached = $router->toArray();
}
/**
* @Subject
*/
public function benchRoute()
{
$router = new Router;
foreach ($this->routes as $index => $rule) {
$router->route('rule'.$index, $rule);
}
$router->match('authors/test/details');
}
/**
* @Subject
*/
public function benchRouteCached()
{
$router = new Router;
$router->fromArray($this->routesCached);
$router->match('authors/test/details');
}
}
================================================
FILE: tests/Database/DongleTest.php
================================================
parseConcat("concat(first_name, ' ', last_name)");
$this->assertEquals("first_name || ' ' || last_name", $result);
$result = $dongle->parseConcat("CONCAT( first_name , ' ', last_name )");
$this->assertEquals("first_name || ' ' || last_name", $result);
$result = $dongle->parseConcat('concat("#", id, " - ", amount, "(", currency_code, ")")');
$this->assertEquals('"#" || id || " - " || amount || "(" || currency_code || ")"', $result);
$result = $dongle->parseConcat("concat(year, ' ', make , ' ' , model)");
$this->assertEquals("year || ' ' || make || ' ' || model", $result);
$result = $dongle->parseConcat("concat(last_name, ', ', first_name)");
$this->assertEquals("last_name || ', ' || first_name", $result);
$result = $dongle->parseConcat("concat(',', last_name, ' , ', first_name, ',')");
$this->assertEquals("',' || last_name || ' , ' || first_name || ','", $result);
$result = $dongle->parseConcat("concat(last_name, ',\' ', first_name)");
$this->assertEquals("last_name || ',\' ' || first_name", $result);
$result = $dongle->parseConcat("group_concat(first_name, ' ', last_name)");
$this->assertEquals("group_concat(first_name, ' ', last_name)", $result);
}
public function testSqliteParseGroupConcat()
{
$dongle = new Dongle('sqlite');
$result = $dongle->parseGroupConcat("group_concat(first_name separator ', ')");
$this->assertEquals("group_concat(first_name, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(sometable.first_name SEPARATOR ', ')");
$this->assertEquals("group_concat(sometable.first_name, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(id separator ')')");
$this->assertEquals("group_concat(id, ')')", $result);
// @todo
// $result = $dongle->parseGroupConcat("group_concat(id order by name separator ',')");
// $this->assertEquals("group_concat(id, ',') OVER (order by name)", $result);
}
public function testPgsqlParseGroupConcat()
{
$dongle = new Dongle('pgsql');
$result = $dongle->parseGroupConcat("group_concat(first_name separator ', ')");
$this->assertEquals("string_agg(first_name::VARCHAR, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(sometable.first_name SEPARATOR ', ')");
$this->assertEquals("string_agg(sometable.first_name::VARCHAR, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(id separator ')')");
$this->assertEquals("string_agg(id::VARCHAR, ')')", $result);
}
public function testSqlSrvParseGroupConcat()
{
$dongle = new Dongle('sqlsrv');
$result = $dongle->parseGroupConcat("group_concat(first_name separator ', ')");
$this->assertEquals("dbo.GROUP_CONCAT_D(first_name, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(sometable.first_name SEPARATOR ', ')");
$this->assertEquals("dbo.GROUP_CONCAT_D(sometable.first_name, ', ')", $result);
$result = $dongle->parseGroupConcat("group_concat(id separator ')')");
$this->assertEquals("dbo.GROUP_CONCAT_D(id, ')')", $result);
}
public function testSqliteParseBooleanExpression()
{
$dongle = new Dongle('sqlite');
$result = $dongle->parseBooleanExpression("select * from table where is_true = true");
$this->assertEquals("select * from table where is_true = 1", $result);
$result = $dongle->parseBooleanExpression("is_true = true and is_false <> true");
$this->assertEquals("is_true = 1 and is_false <> 1", $result);
$result = $dongle->parseBooleanExpression("is_true = true and is_false = false or is_whatever = 2");
$this->assertEquals("is_true = 1 and is_false = 0 or is_whatever = 2", $result);
$result = $dongle->parseBooleanExpression("select * from table where is_true = true");
$this->assertEquals("select * from table where is_true = 1", $result);
}
public function testSqlSrvParseIfNull()
{
$dongle = new Dongle('sqlsrv');
$result = $dongle->parseIfNull("select ifnull(1,0) from table");
$this->assertEquals("select isnull(1,0) from table", $result);
$result = $dongle->parseIfNull("select IFNULL(1,0) from table");
$this->assertEquals("select isnull(1,0) from table", $result);
}
public function testPgSrvParseIfNull()
{
$dongle = new Dongle('pgsql');
$result = $dongle->parseIfNull("select ifnull(1,0) from table");
$this->assertEquals("select coalesce(1,0) from table", $result);
$result = $dongle->parseIfNull("select IFNULL(1,0) from table");
$this->assertEquals("select coalesce(1,0) from table", $result);
}
}
================================================
FILE: tests/Database/ModelAddersTest.php
================================================
assertEquals(['id' => 'int'], $model->getCasts());
$model->addCasts(['foo' => 'int']);
$this->assertEquals(['id' => 'int', 'foo' => 'int'], $model->getCasts());
}
}
================================================
FILE: tests/Database/SortableTest.php
================================================
addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => ''
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
}
public function testOrderByIsAutomaticallyAdded()
{
$model = new TestModel;
$query = $model->newQuery()->toSql();
$this->assertEquals('select * from "test" order by "test"."sort_order" asc', $query);
}
public function testOrderByCanBeOverridden()
{
$model = new TestModel;
$query1 = $model->newQuery()->orderBy('name')->orderBy('email', 'desc')->toSql();
$query2 = $model->newQuery()->orderBy('sort_order')->orderBy('name')->toSql();
$this->assertEquals('select * from "test" order by "name" asc, "email" desc', $query1);
$this->assertEquals('select * from "test" order by "sort_order" asc, "name" asc', $query2);
}
}
class TestModel extends \October\Rain\Database\Model
{
use \October\Rain\Database\Traits\Sortable;
protected $table = 'test';
}
================================================
FILE: tests/Database/Traits/EncryptableTest.php
================================================
fill(['secret' => 'test']);
$this->assertEquals('test', $testModel->secret);
$this->assertNotEquals('test', $testModel->attributes['secret']);
$payloadOne = json_decode(base64_decode($testModel->attributes['secret']), true);
$this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadOne));
// Don't encrypt empty strings
$testModel->secret = '';
$this->assertEquals('', $testModel->secret);
$this->assertEquals('', $testModel->attributes['secret']);
// Encrypt numerics
$testModel->secret = 0;
$this->assertEquals(0, $testModel->secret);
$this->assertNotEquals(0, $testModel->attributes['secret']);
$payloadTwo = json_decode(base64_decode($testModel->attributes['secret']), true);
$this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadTwo));
$this->assertNotEquals($payloadOne['value'], $payloadTwo['value']);
// Test reset
$testModel->secret = null;
$this->assertNull($testModel->secret);
$this->assertNull($testModel->attributes['secret']);
}
}
class TestModelEncryptable extends \October\Rain\Database\Model
{
use \October\Rain\Database\Traits\Encryptable;
protected $encryptable = ['secret'];
protected $fillable = ['secret'];
protected $table = 'secrets';
}
================================================
FILE: tests/Database/Traits/SluggableTest.php
================================================
addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => ''
]);
// Create the dataset in the connection with the tables
$capsule->setAsGlobal();
$capsule->bootEloquent();
$capsule->schema()->create('test_sluggable', function ($table) {
$table->increments('id');
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// Mock app instance for this test
App::swap(new class {
public function getLocale() { return 'en'; }
});
}
/**
* testSlugGeneration
*/
public function testSlugGeneration()
{
$testModel1 = TestModelSluggable::create(['name' => 'test']);
$this->assertEquals($testModel1->slug, 'test');
$testModel2 = TestModelSluggable::create(['name' => 'test']);
$this->assertEquals($testModel2->slug, 'test-2');
$testModel3 = TestModelSluggable::create(['name' => 'test']);
$this->assertEquals($testModel3->slug, 'test-3');
}
}
/**
* TestModelSluggable example class
*/
class TestModelSluggable extends Model
{
use \October\Rain\Database\Traits\Sluggable;
protected $slugs = ['slug' => 'name'];
protected $fillable = ['name'];
protected $table = 'test_sluggable';
}
================================================
FILE: tests/Database/Traits/ValidationTest.php
================================================
'unique', 'email' => 'unique:users'];
$this->exists = true;
$this->assertEquals([
'name' => ['unique:users,name,7,the_id'],
'email' => ['unique:users,email,7,the_id']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'name' => ['unique:users'],
'email' => ['unique:users']
], $this->processValidationRules($rules));
// Custom database connection
$rules = ['email' => 'unique:myconnection.users'];
$this->exists = true;
$this->assertEquals([
'email' => ['unique:myconnection.users,email,7,the_id']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'email' => ['unique:myconnection.users']
], $this->processValidationRules($rules));
// Custom table column name
$rules = ['email' => 'unique:users,email_address'];
$this->exists = true;
$this->assertEquals([
'email' => ['unique:users,email_address,7,the_id']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'email' => ['unique:users,email_address']
], $this->processValidationRules($rules));
// Forcing a unique rule to ignore a given ID
$rules = ['email' => 'unique:users,email_address,10'];
$this->exists = true;
$this->assertEquals([
'email' => ['unique:users,email_address,7,the_id']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'email' => ['unique:users,email_address,10']
], $this->processValidationRules($rules));
// Adding additional where clauses
$rules = ['email' => 'unique:users,email_address,NULL,id,account_id,1'];
$this->exists = true;
$this->assertEquals([
'email' => ['unique:users,email_address,20,id,account_id,1']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'email' => ['unique:users,email_address,NULL,id,account_id,1']
], $this->processValidationRules($rules));
// Adding multiple additional where clauses
$rules = ['email' => 'unique:users,email_address,NULL,id,account_id,1,account_name,"Foo",user_id,3'];
$this->exists = true;
$this->assertEquals([
'email' => ['unique:users,email_address,20,id,account_id,1,account_name,"Foo",user_id,3']
], $this->processValidationRules($rules));
$this->exists = false;
$this->assertEquals([
'email' => ['unique:users,email_address,NULL,id,account_id,1,account_name,"Foo",user_id,3']
], $this->processValidationRules($rules));
}
protected function getTable()
{
return 'users';
}
protected function getConnectionName()
{
return 'mysql';
}
protected function getKey()
{
return 7;
}
protected function getKeyName()
{
return 'the_id';
}
public function testArrayFieldNames()
{
$mock = $this->getMockForTrait(\October\Rain\Database\Traits\Validation::class);
$rules = [
'field' => 'required',
'field.two' => 'required|boolean',
'field[three]' => 'required|date',
'field[three][child]' => 'required',
'field[four][][name]' => 'required',
'field[five' => 'required|string',
'field][six' => 'required|string',
'field]seven' => 'required|string',
];
$rules = self::callProtectedMethod($mock, 'processRuleFieldNames', [$rules]);
$this->assertEquals([
'field' => 'required',
'field.two' => 'required|boolean',
'field.three' => 'required|date',
'field.three.child' => 'required',
'field.four.*.name' => 'required',
'field[five' => 'required|string',
'field][six' => 'required|string',
'field]seven' => 'required|string',
], $rules);
}
}
================================================
FILE: tests/Database/UpdaterTest.php
================================================
updater = new Updater();
}
public function testClassNameGetsParsedCorrectly()
{
$reflector = new ReflectionClass(TestPlugin\SampleClass::class);
$filePath = $reflector->getFileName();
$classFullName = $this->updater->getClassFromFile($filePath);
$this->assertEquals(TestPlugin\SampleClass::class, $classFullName);
}
}
================================================
FILE: tests/Events/EventDispatcherTest.php
================================================
setLaravelDispatcher(new Dispatcher);
Event::swap(new FakeDispatcher($dispatcher));
Event::fire(EventDispatcherTest::class);
Event::assertDispatched(EventDispatcherTest::class);
}
}
================================================
FILE: tests/Extension/ExtendableTest.php
================================================
assertNull($subject->classAttribute);
ExtendableTestExampleExtendableClass::extend(function ($extension) {
$extension->classAttribute = 'bar';
});
$subject = new ExtendableTestExampleExtendableClass;
$this->assertEquals('bar', $subject->classAttribute);
}
public function testSettingDeclaredPropertyOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->classAttribute = 'Test';
$this->assertEquals('Test', $subject->classAttribute);
}
// public function testSettingUndeclaredPropertyOnClass()
// {
// $this->expectException(\BadMethodCallException::class);
// $this->expectExceptionMessage("Call to undefined property ExtendableTestExampleExtendableClass::newAttribute");
// $subject = new ExtendableTestExampleExtendableClass;
// $subject->newAttribute = 'Test';
// }
public function testSettingDeclaredPropertyOnBehavior()
{
$subject = new ExtendableTestExampleExtendableClass;
$behavior = $subject->getClassExtension('ExtendableTestExampleBehaviorClass1');
$subject->behaviorAttribute = 'Test';
$this->assertEquals('Test', $subject->behaviorAttribute);
$this->assertEquals('Test', $behavior->behaviorAttribute);
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
}
public function testDynamicPropertyOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$this->assertFalse($subject->propertyExists('newAttribute'));
$subject->addDynamicProperty('dynamicAttribute', 'Test');
$this->assertEquals('Test', $subject->dynamicAttribute);
$this->assertTrue($subject->propertyExists('dynamicAttribute'));
}
public function testDynamicallyImplementingClass()
{
ExtendableTestExampleImplementableClass::extend(function($obj) {
$obj->implementClassWith('ExtendableTestExampleBehaviorClass2');
$obj->implementClassWith('ExtendableTestExampleBehaviorClass2');
$obj->implementClassWith('ExtendableTestExampleBehaviorClass2');
});
$subject = new ExtendableTestExampleImplementableClass;
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));
}
public function testDynamicallyExtendingClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->extendClassWith('ExtendableTestExampleBehaviorClass2');
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));
}
public function testDynamicMethodOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');
$this->assertEquals('foo', $subject->getFoo());
$this->assertEquals('foo', $subject->getFooAnotherWay());
}
public function testDynamicExtendAndMethodOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->extendClassWith('ExtendableTestExampleBehaviorClass2');
$subject->addDynamicMethod('getOriginalFoo', 'getFoo', 'ExtendableTestExampleBehaviorClass1');
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
$this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));
$this->assertEquals('bar', $subject->getFoo());
$this->assertEquals('foo', $subject->getOriginalFoo());
}
public function testDynamicClosureOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->addDynamicMethod('sayHello', function () {
return 'Hello world';
});
$this->assertEquals('Hello world', $subject->sayHello());
}
public function testDynamicCallableOnClass()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->addDynamicMethod('getAppName', ['ExtendableTestExampleClass', 'getName']);
$this->assertEquals('october', $subject->getAppName());
}
public function testCallingStaticMethod()
{
$result = ExtendableTestExampleExtendableClass::getStaticBar();
$this->assertEquals('bar', $result);
$result = ExtendableTestExampleExtendableClass::vanillaIceIce();
$this->assertEquals('baby', $result);
}
public function testCallingUndefinedStaticMethod()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::undefinedMethod()');
$result = ExtendableTestExampleExtendableClass::undefinedMethod();
$this->assertEquals('bar', $result);
}
// public function testAccessingProtectedProperty()
// {
// $this->expectException(BadMethodCallException::class);
// $this->expectExceptionMessage('Call to undefined property ExtendableTestExampleExtendableClass::protectedFoo');
// $subject = new ExtendableTestExampleExtendableClass;
// $this->assertEmpty($subject->protectedFoo);
// $subject->protectedFoo = 'snickers';
// $this->assertEquals('bar', $subject->getProtectedFooAttribute());
// }
public function testAccessingProtectedMethod()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::protectedBar()');
$subject = new ExtendableTestExampleExtendableClass;
echo $subject->protectedBar();
}
public function testAccessingProtectedStaticMethod()
{
$this->expectException(BadMethodCallException::class);
$this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::protectedMars()');
echo ExtendableTestExampleExtendableClass::protectedMars();
}
public function testInvalidImplementValue()
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Class ExtendableTestInvalidExtendableClass contains an invalid $implement value');
$result = new ExtendableTestInvalidExtendableClass;
}
public function testSoftImplementFake()
{
$result = new ExtendableTestExampleExtendableSoftImplementFakeClass;
$this->assertFalse($result->isClassExtendedWith('RabbleRabbleRabble'));
$this->assertEquals('working', $result->getStatus());
}
public function testSoftImplementReal()
{
$result = new ExtendableTestExampleExtendableSoftImplementRealClass;
$this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
$this->assertEquals('foo', $result->getFoo());
}
public function testSoftImplementCombo()
{
$result = new ExtendableTestExampleExtendableSoftImplementComboClass;
$this->assertFalse($result->isClassExtendedWith('RabbleRabbleRabble'));
$this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));
$this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));
$this->assertEquals('bar', $result->getFoo()); // ExtendableTestExampleBehaviorClass2 takes priority, defined last
}
public function testDotNotation()
{
$subject = new ExtendableTestExampleExtendableClassDotNotation();
$subject->extendClassWith('ExtendableTest.ExampleBehaviorClass2');
$this->assertTrue($subject->isClassExtendedWith('ExtendableTest.ExampleBehaviorClass1'));
$this->assertTrue($subject->isClassExtendedWith('ExtendableTest.ExampleBehaviorClass2'));
}
public function testMethodExists()
{
$subject = new ExtendableTestExampleExtendableClass;
$this->assertTrue($subject->methodExists('extend'));
}
public function testMethodNotExists()
{
$subject = new ExtendableTestExampleExtendableClass;
$this->assertFalse($subject->methodExists('missingFunction'));
}
public function testDynamicMethodExists()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');
$this->assertTrue($subject->methodExists('getFooAnotherWay'));
}
public function testGetClassMethods()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');
$methods = $subject->getClassMethods();
$this->assertContains('extend', $methods);
$this->assertContains('getFoo', $methods);
$this->assertContains('getFooAnotherWay', $methods);
$this->assertNotContains('missingFunction', $methods);
}
public function testIsInstanceOf()
{
$subject1 = new ExtendableTestExampleExtendableClass;
$subject2 = new ExtendableTestExampleExtendableSoftImplementFakeClass;
$subject3 = new ExtendableTestExampleExtendableSoftImplementRealClass;
$this->assertTrue($subject1->isClassInstanceOf(ExampleExtendableInterface::class));
$this->assertFalse($subject2->isClassInstanceOf(ExampleExtendableInterface::class));
$this->assertTrue($subject3->isClassInstanceOf(ExampleExtendableInterface::class));
}
public function testDynamicExtendOverridesMethod()
{
$subject = new ExtendableTestExampleExtendableClass;
$this->assertEquals('foo', $subject->getFoo());
$subject->extendClassWith('ExtendableTestExampleBehaviorClass2');
$this->assertEquals('bar', $subject->getFoo());
}
public function testDynamicExtendOverrideAsExtension()
{
$subject = new ExtendableTestExampleExtendableClass;
$subject->extendClassWith('ExtendableTestExampleBehaviorClass2');
// Default call resolves to the last registered behavior
$this->assertEquals('bar', $subject->getFoo());
// asExtension can still reach the original behavior
$this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());
$this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());
}
public function testMultipleBehaviorsMethodPriority()
{
$subject = new ExtendableTestExampleExtendableMultiBehaviorClass;
// BehaviorClass2 is last in $implement, so it wins
$this->assertEquals('bar', $subject->getFoo());
// Both behaviors are still accessible directly
$this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());
$this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());
}
public function testDynamicExtendOverridesImplementMethod()
{
$subject = new ExtendableTestExampleExtendableMultiBehaviorClass;
$this->assertEquals('bar', $subject->getFoo());
// Dynamic extension overrides both implemented behaviors
$subject->extendClassWith('ExtendableTestExampleBehaviorClass3');
$this->assertEquals('baz', $subject->getFoo());
// All three behaviors remain accessible via asExtension
$this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());
$this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());
$this->assertEquals('baz', $subject->asExtension('ExtendableTestExampleBehaviorClass3')->getFoo());
}
}
//
// Test classes
//
interface ExampleExtendableInterface
{
public function hasPanda();
}
/**
* Example behavior classes
*/
class ExtendableTestExampleBehaviorClass1 extends ExtensionBase
{
public $behaviorAttribute;
public function getFoo()
{
return 'foo';
}
public static function getStaticBar()
{
return 'bar';
}
public static function vanillaIceIce()
{
return 'cream';
}
public function hasPanda()
{
return true;
}
}
class ExtendableTestExampleBehaviorClass2 extends ExtensionBase
{
public $behaviorAttribute;
public function getFoo()
{
return 'bar';
}
}
/*
* Example class that has an invalid implementation
*/
class ExtendableTestInvalidExtendableClass extends Extendable
{
public $implement = 24;
public $classAttribute;
}
/*
* Example class that has extensions enabled
*/
class ExtendableTestExampleExtendableClass extends Extendable
{
public $implement = ['ExtendableTestExampleBehaviorClass1'];
public $classAttribute;
protected $protectedFoo = 'bar';
public static function vanillaIceIce()
{
return 'baby';
}
protected function protectedBar()
{
return 'foo';
}
protected static function protectedMars()
{
return 'bar';
}
public function getProtectedFooAttribute()
{
return $this->protectedFoo;
}
}
/**
* ExtendableTestExampleImplementableClass
*/
class ExtendableTestExampleImplementableClass extends Extendable
{
public $implement = ['ExtendableTestExampleBehaviorClass1'];
}
/**
* A normal class without extensions enabled
*/
class ExtendableTestExampleClass
{
public static function getName()
{
return 'october';
}
}
/*
* Example class with soft implement failure
*/
class ExtendableTestExampleExtendableSoftImplementFakeClass extends Extendable
{
public $implement = ['@RabbleRabbleRabble'];
public static function getStatus()
{
return 'working';
}
}
/*
* Example class with soft implement success
*/
class ExtendableTestExampleExtendableSoftImplementRealClass extends Extendable
{
public $implement = ['@ExtendableTestExampleBehaviorClass1'];
}
/*
* Example class with soft implement hybrid
*/
class ExtendableTestExampleExtendableSoftImplementComboClass extends Extendable
{
public $implement = [
'ExtendableTestExampleBehaviorClass1',
'@ExtendableTestExampleBehaviorClass2',
'@RabbleRabbleRabble'
];
}
/*
* Example class that has extensions enabled using dot notation
*/
class ExtendableTestExampleExtendableClassDotNotation extends Extendable
{
public $implement = ['ExtendableTest.ExampleBehaviorClass1'];
public $classAttribute;
protected $protectedFoo = 'bar';
public static function vanillaIceIce()
{
return 'baby';
}
protected function protectedBar()
{
return 'foo';
}
protected static function protectedMars()
{
return 'bar';
}
public function getProtectedFooAttribute()
{
return $this->protectedFoo;
}
}
class ExtendableTestExampleBehaviorClass3 extends ExtensionBase
{
public function getFoo()
{
return 'baz';
}
}
/*
* Example class with multiple behaviors that define the same method
*/
class ExtendableTestExampleExtendableMultiBehaviorClass extends Extendable
{
public $implement = [
'ExtendableTestExampleBehaviorClass1',
'ExtendableTestExampleBehaviorClass2',
];
}
/*
* Add namespaced aliases for dot notation test
*/
class_alias('ExtendableTestExampleBehaviorClass1', 'ExtendableTest\\ExampleBehaviorClass1');
class_alias('ExtendableTestExampleBehaviorClass2', 'ExtendableTest\\ExampleBehaviorClass2');
================================================
FILE: tests/Extension/ExtensionTest.php
================================================
assertEquals('foo', $subject->behaviorAttribute);
ExtensionTestExampleBehaviorClass1::extend(function ($extension) {
$extension->behaviorAttribute = 'bar';
});
$subject = new ExtensionTestExampleExtendableClass;
$this->assertEquals('bar', $subject->behaviorAttribute);
}
}
/*
* Example class that has extensions enabled
*/
class ExtensionTestExampleExtendableClass extends Extendable
{
public $implement = ['ExtensionTestExampleBehaviorClass1'];
}
/**
* Example behavior classes
*/
class ExtensionTestExampleBehaviorClass1 extends ExtensionBase
{
public $behaviorAttribute = 'foo';
}
================================================
FILE: tests/Halcyon/DatasourceResolverTest.php
================================================
$theme1,
'theme2' => $theme2,
'theme3' => $theme3
]);
$this->assertTrue($resolver->hasDatasource('theme1'));
$this->assertTrue($resolver->hasDatasource('theme2'));
$this->assertTrue($resolver->hasDatasource('theme3'));
$this->assertFalse($resolver->hasDatasource('theme4'));
}
public function testDefaultDatasource()
{
$resolver = new Resolver;
$resolver->setDefaultDatasource('theme1');
$this->assertEquals('theme1', $resolver->getDefaultDatasource());
}
}
================================================
FILE: tests/Halcyon/HalcyonModelTest.php
================================================
setDatasourceResolver();
$this->setValidatorOnModel();
}
public function testFindAll()
{
$pages = HalcyonTestPage::all();
$this->assertCount(4, $pages);
$this->assertContains('about.htm', $pages->lists('fileName'));
$this->assertContains('home.htm', $pages->lists('fileName'));
$this->assertContains('level1/team.htm', $pages->lists('fileName'));
$this->assertContains('level1/level2/level3/level4/level5/contact.htm', $pages->lists('fileName'));
}
public function testFindPage()
{
$page = HalcyonTestPage::find('home');
$this->assertNotNull($page);
$this->assertCount(6, $page->attributes);
$this->assertArrayHasKey('fileName', $page->attributes);
$this->assertEquals('home.htm', $page->fileName);
$this->assertCount(1, $page->settings);
$this->assertEquals('
World!
', $page->markup);
$this->assertEquals('hello', $page->title);
}
public function testFindMenu()
{
$menu = HalcyonTestMenu::find('mainmenu');
$this->assertNotNull($menu);
$this->assertEquals('
Home
', $menu->content);
}
public function testOtherDatasourcePage()
{
$page = HalcyonTestPage::on('theme2')->find('home');
$this->assertNotNull($page);
$this->assertCount(6, $page->attributes);
$this->assertArrayHasKey('fileName', $page->attributes);
$this->assertEquals('home.htm', $page->fileName);
$this->assertCount(1, $page->settings);
$this->assertEquals('