Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 05/12/2020 in all areas

  1. I've seen a couple of questions regarding namespaces and autoloading floating around the forum recently, so I decided to write a little tutorial. In general, I often see people getting confused when they try to wrap their head around namespaces, autoloading, Composer and the mapping of namespaces to directory structures all at once. In fact, those are very much independent, distinct concepts, and it is much easier to explain and understand them separately. So this guide is structured as follows: How namespaces work in PHP. How autoloading works in PHP. Conventions for mapping namespaces to directory structures: PSR-4. How autoloading works in Composer and ProcessWire's class loader. How to use the class loader in a ProcessWire module. Feel free to skip the sections you're already familiar with. Namespaces in PHP The purpose of namespaces in PHP is to avoid naming conflicts between classes, functions and constants, especially when you're using external libraries and frameworks. Nothing more. It's important to understand that this has nothing at all to do with autoloading, directory structures or file names. You can put namespaced stuff everywhere you want. You can even have multiple namespaces inside a single file (don't try this at home). Namespaces only exist to be able to use a generic name – for example, ProcessWire's Config class – multiple times in different contexts without getting a naming conflict. Without namespaces, I couldn't use any library that includes a Config class of it's own, because that name is already taken. With namespaces, you can have a distinction between the classes ProcessWire\Config and MoritzLost\Config. You can also use sub-namespaces to further segregate your code into logical groups. For example, I can have two classes MoritzLost\Frontend\Config and MoritzLost\Backend\Config– a class name only needs to be unique within it's namespace. You can declare the namespace for a PHP file using the namespace statement at the top: // file-one.php <?php namespace ProcessWire; // file-two.php <?php namespace MoritzLost\Frontend; This way, all classes, methods and constants defined inside this file are placed in that namespace. All ProcessWire classes live in the ProcessWire namespace. Now to use one of those classes – for example, to instantiate it – you have a couple of options. You can either use it's fully qualified class name or import it into the current namespace. Also, if you are inside a namespaced file, any reference to a class is relative to that namespace. Unless it starts with a backward slash, in this case it's relative to the global namespace. So all of those examples are equivalent: // example-one.php <?php namespace ProcessWire; $page = new Page(); // example-two.php <?php use ProcessWire\Page; $page = new Page(); // example-three.php <?php $page = new ProcessWire\Page(); // example-four.php <?php namespace MoritzLost\Somewhere\Over\The\Rainbow; $page = new \ProcessWire\Page(); The use statement in the second example can be read like this: “Inside this file, all references to Page refer to the class \ProcessWire\Page” How autoloading works Every PHP program starts with one entry file – for ProcessWire, that's usually it's index.php. But you don't want to keep all your code in one file, that would get out of hand quickly. Once you start to split your code into several individual files however, you have to take care of manually including them with require or include calls. That becomes very tedious as well. The purpose of autoloading is to be able to add new code in new files without having to import them manually. This, again, has nothing to do with namespaces, not even something with file locations. Autoloading is a pretty simple concept: If you try to use a class that hasn't been loaded yet, PHP calls upon it's registered autoloaders as a last-ditch attempt to load them before throwing an exception. Let's look at a simple example: // classes.php <?php class A { /** class stuff */ } class B { /** class stuff */ } // index.php <?php spl_autoload_register(function ($class) { include_once 'classes.php'; }); new A(); new B(); This is a complete and functional autoloader. If you don't believe me, go ahead and save those two files (classes.php and index.php) and run the index.php with php -f index.php. Then comment out the include_once call and run it again, then you'll get an error that class A was not found. Now here's what happens when index.php is executed (with the autoloader active): Our anonymous function is added to the autoload queue through spl_autoload_register. PHP tries to instantiate class A, but can't because it's not loaded yet. If there was no autoloader registered, the program would die with a fatal error at this point. But since there is an autoloader ... The autoloader is called. Our autoloader includes classes.php with the class definition. That was a close one! Since the class has been loaded, execution goes back to the index.php which can now proceed to instantiate A and B. If the class was still not loaded at this point, PHP would go back to the original plan and die. One thing to note is that the autoloader will only be called once in this example. That's because both A and B are in the same file and that file is included during the first call to the autoloader. Autoloading works on files, not on classes! The important takeaway is that PHP doesn't know if the autoloader knows where to find the class it asks for or, if there are multiple autoloader, which one can load it. PHP just calls each registered autoloader in turn and checks if the class has been loaded after each one. If the class still isn't loaded after the last autoloader is done, it's error time. What the autoloader actually does is pretty much wild wild west as well. It takes the name of the class PHP is trying to load as an argument, but it doesn't have to do anything with it. Our autoloader ignores it entirely. Instead, it just includes classes.php and says to itself “My job here is done”. If class A was in another file, it wouldn't have worked. This process has two main advantages: Since autoloaders are only called on-demand to load classes just in time, we only include the files we actually need. If in the example above class A and B are not used in some scenarios, the classes.php will not be included, which will result in better performance for larger projects (though this isn't as cut and dry, since autoloading has it's own overhead, so if you load most classes anyway during a single request, it will actually be less efficient). If the autoloader is smart enough to somehow map class names to the files they're located in, we can just let the autoloader handle including the classes we need, without having to worry about jamming include statements everywhere. That brings us to ... PSR-4, namespaces and directory structures As you see, namespaces and autoloading are both pretty limited concepts. And they aren't inherently linked to each other. You can namespace your classes without ever adding an autoloader, and you can autoload classes that are all in the same namespace. But they become useful when you put them together. At the core of all that autoloading talk is a simple idea: By putting classes in files named after their class names, and putting those files in directory hierarchies based on the namespace hierarchy, the autoloader can efficiently find and load those files based on the namespace. All it needs is a list of root namespaces with their corresponding directories. The exact way class names and namespaces are mapped to directory structures and file names is purely conventional. The accepted convention for this is PSR-4. This is a super simple standard which basically just sums up the ideas above: A base namespace is mapped to a specific directory in the file system. When the autoloader is asked to load a class in that namespace (or a sub-namespace of it), it starts looking in that folder. This "base" namespace may include multiple parts – for example, I could use MoritzLost\MyAwesomeLibrary as a base and map that to my source directory. PSR-4 calls this a "namespace prefix". Each sub-namespace corresponds to a sub-directory. So by looking at the namespace, you can follow subdirectories to the location where you expect to find the class file. Finally, the class name is mapped directly to the file name. So MyCoolClass needs to be put inside MyCoolClass.php. This all sounds simple and straightforward - and it absolutely is! It's only once you mash everything together, mix up language features, accepted conventions and proprietary implementations like Composer on top that it becomes hard to grasp in one go. Composer and ProcessWire's class loader Now all that's left is to talk about how Composer and ProcessWire provide autoloading. Composer, of course, is primarily a tool for dependency management. But because most libraries use namespaces and most developers want to have the libraries they're using autoloaded, those topics become a prerequisite to understanding what Composer does in this regard. Composer can use different autoloading mechanisms; for example, you can just give it a static list of files to include for every request, or use the older PSR-0 standard. But most modern libraries use PSR-4 to autoload classes. So all Composer needs to function is a mapping of namespace prefixes to directories. Each library maintains this mapping for it's PSR-4-structured classes through the autoload information in their composer.json. You can do this for your own site to: Just include the autoload information as shown in the documentation and point it to the directory of your class files. Composer collects all that information and uses it to generate a custom file at vendor/autoload.php — that's the one you need to include somewhere whenever you set up Composer in one of your projects. Bells and whistles aside, this file just registers an autoloader function that will use all the information collected from your own and your included libraries' composer.json to locate and include class files on demand. You can read more about how to optimize Composer's autoloader for production usage here. If you want to read up on how to set up Composer for your own sites, read my ProcessWire + Composer integration guide instead. And finally, what does ProcessWire do to handle all this? Turns out, ProcessWire has it's own autoloader implementation that is more or less PSR-4 compliant. You can access it as an API variable ($classLoader or wire('classLoader'), depending on context). Instead of using a static configuration file like Composer, the namespace -> directory mapping is added during the runtime by calling $classLoader->addNamespace. As you would expect, this function accepts a namespace and a directory path. You can use this to register your own custom namespaces. Alternatively, if you have site-specific classes within the ProcessWire namespace, you can just add their location to the class loader using the same method: $classLoader->addNamespace('ProcessWire', '/path/to/your/classes/'). Utilizing custom namespaces and autoloading in ProcessWire modules Now as a final remark, I wanted to give an example of how to use custom namespaces and the class loader in your own modules. I'll use my TrelloWire module as an example: Decide what namespace you're going to use. The main module file should live in the ProcessWire namespace, but if you have other classes in your module, they can and should use a custom namespace to avoid collisions with other modules. TrelloWire uses ProcessWire\TrelloWire, but you can also use something outside the ProcessWire namespace. You need to make sure to add the namespace to the class loader as early as possible. If either you or a user of your module tries to instantiate one of your custom classes before that, it will fail. Good places to start are the constructor of your main module file, or their init or ready methods. Here's a complete example. The module uses only one custom namespaced class: ProcessWire\TrelloWire\TrelloWireApi, located in the src/ directory of the module. But with this setup, I can add more classes whenever I need without having to modify anything else. /** * The constructor registers the TrelloWire namespace used by this module. */ public function __construct() { $namespace = 'ProcessWire\\TrelloWire'; $classLoader = $this->wire('classLoader'); if (!$classLoader->hasNamespace($namespace)) { $srcPath = $this->wire('config')->paths->get($this) . 'src/'; $classLoader->addNamespace($namespace, $srcPath); } } Source Thanks for making it through to the very end! I gotta learn to keep those things short. Anyway, I hope this clears up some questions about namespaces and autoloading. Let me know if I got something wrong, and feel free to add your own tips and tricks!
    11 points
  2. We use RockMigrations regularly for adding custom functionality, pages, templates and fields to new projects and update the existing sites modules. Works a treat.
    2 points
  3. You need to handle 404 errors yourself. Look here for a working example.
    2 points
  4. Hey folks! Took a couple of late nights, but managed to turn this old gist of mine into a proper module. The name is SearchEngine, and currently it provides support for indexing page contents (into a hidden textarea field created automatically), and also includes a helper feature ("Finder") for querying said contents. No fancy features like stemming here yet, but something along those lines might be added later if it seems useful (and if I find a decent implementation to integrate). Though the API and selector engine make it really easy to create site search pages, I pretty much always end up duplicating the same features from site to site. Also – since it takes a bit of extra time – it's tempting to skip over some accessibility related things, and leave features like text highlighting out. Overall I think it makes sense to bundle all that into a module, which can then be reused over and over again ? Note: markup generation is not yet built into the module, which is why the examples below use PageArray::render() method to produce a simple list of results. This will be added later on, as a part of the same module or a separate Markup module. There's also no fancy JS API or anything like that (yet). This is an early release, so be kind – I got the find feature working last night (or perhaps this morning), and some final tweaks and updates were made just an hour ago ? GitHub repository: https://github.com/teppokoivula/SearchEngine Modules directory: https://modules.processwire.com/modules/search-engine/ Demo: https://wireframe-framework.com/search/ Usage Install SearchEngine module. Note: the module will automatically create an index field install time, so be sure to define a custom field (via site config) before installation if you don't want it to be called "search_index". You can change the field name later as well, but you'll have to update the "index_field" option in site config or module settings (in Admin) after renaming it. Add the site search index field to templates you want to make searchable. Use selectors to query values in site search index. Note: you can use any operator for your selectors, you will likely find the '=' and '%=' operators most useful here. You can read more about selector operators from ProcessWire's documentation. Options By default the module will create a search index field called 'search_index' and store values from Page fields title, headline, summary, and body to said index field when a page is saved. You can modify this behaviour (field name and/or indexed page fields) either via the Module config screen in the PocessWire Admin, or by defining $config->SearchEngine array in your site config file or other applicable location: $config->SearchEngine = [ 'index_field' => 'search_index', 'indexed_fields' => [ 'title', 'headline', 'summary', 'body', ], 'prefixes' => [ 'link' => 'link:', ], 'find_args' => [ 'limit' => 25, 'sort' => 'sort', 'operator' => '%=', 'query_param' => null, 'selector_extra' => '', ], ]; You can access the search index field just like any other ProcessWire field with selectors: if ($q = $sanitizer->selectorValue($input->get->q)) { $results = $pages->find('search_index%=' . $query_string . ', limit=25'); echo $results->render(); echo $results->renderPager(); } Alternatively you can delegate the find operation to the SearchEngine module: $query = $modules->get('SearchEngine')->find($input->get->q); echo $query->resultsString; // alias for $query->results->render() echo $query->pager; // alias for $query->results->renderPager() Requirements ProcessWire >= 3.0.112 PHP >= 7.1.0 Note: later versions of the module may require Composer, or alternatively some additional features may require installing via Composer. This is still under consideration – so far there's nothing here that would really depend on it, but advanced features like stemming most likely would. Installing It's the usual thing: download or clone the SearchEngine directory into your /site/modules/ directory and install via Admin. Alternatively you can install SearchEngine with Composer by executing composer require teppokoivula/search-engine in your site directory.
    1 point
  5. I had a similar situation a while back, but this thread helped...can't remember any details except that there was a permissions issue in the app's directory structure
    1 point
  6. Did you include the pw index.php in test.php (additionaly processwire namespace could be required. I am not sure. Please try)
    1 point
  7. Thx for the report - always nice to hear that it does not only work for me ? You should definitely take a look at the quite new migrate() method that lets you create fields, templates and pages via simple array syntax ? I'm using this in every module now and it saves me so much time ? /** * Setup this module */ public function setup() { $this->rm->migrate([ 'fields' => [ 'field_done' => [ 'type' => 'textarea', ], ], 'templates' => [ 'template_triggers' => [ 'fields' => ['title'], 'icon' => 'database', 'noParents' => -1, // only one 'childTemplates' => ['template_trigger'], 'noChildren' => 1, // we don't allow pages to be created via backend 'sortfield' => '-created', 'tags' => 'RockTrigger', ], 'template_trigger' => [ 'fields' => [ 'title', 'field_done', ], 'icon' => 'bolt', 'noChildren' => 1, 'parentTemplates' => ['template_triggers'], 'pageClass' => '\RockTrigger\TriggerPage', 'noSettings' => 1, 'tags' => 'RockTrigger', ], ], 'pages' => [ 'rocktriggers' => [ 'title' => "Triggers", 'template' => 'template_triggers', 'parent' => 1, 'status' => ['hidden', 'locked'], ], ], ]); } PS: In my project this setup() method is triggerd on every modules refresh. Maybe a little overhead but for now the easiest option...
    1 point
  8. Hello, Did not tried this. But i thing a scenario as following will work. Install PW under a subpage. Like www.domain.com/pw Create your site as usual. In a php file where you want to use pw include the index of pw like; include("pw/index.php"); This should be enough. (not sure if the namespace needed anycase)
    1 point
  9. Sorry for the convoluted title. I have a problem with Process modules that define a custom page using the page key through getModuleInfo (as demonstrated in this excellent tutorial by @bernhard). Those pages are created automatically when the module is installed. The problem is that the title of the page only gets set in the current language. That's not a problem if the current language (language of the superuser who is installing the module) is the default language; if it isn't, the Process page is missing a title in the default language. This has the very awkward effect that a user using the backend in the default language (or any other language) will see an empty entry in the setup menu: This screenshot comes from my Cache Control module which includes a Process page. Now I realize the description sounds obscure, but for us it's a common setup: We a multiple bilingual sites where the default language is German and the second language is English. While the clients use the CMS in German, as a developer I prefer the English interface, so whenever I install a Process module I get this problem. As a module author, is there a way to handle this situation? I guess it would be possible to use post-installation hooks or create the pages manually, but I very much prefer the declarative approach. The page title is already translatable (through the __ function), but of course at the time of installation there is no translation, and as far as I'm aware it's not possible to ship translations with a module so they are used automatically. Could this situation be handled better in the core? I would prefer if the module installation process would always set the title of the Process page in the default language, instead of the language of the current user.
    1 point
  10. I still do this the old fashioned way ?. I have never gotten round to using the page key syntax. Instead, I do this, i.e. create my module's page title (and other stuff if required) in the ___install() routine. So, this could be a workaround in your case. Here's an example from my Blog module public function ___install() { $pages = $this->wire('pages'); // create Blog Admin page $page = $pages->get('template=admin, name='.self::PAGE_NAME); if (!$page->id) { $page = new Page(); $page->template = 'admin'; $page->parent = $pages->get($this->wire('config')->adminRootPageID); $page->title = 'Blog'; $page->name = self::PAGE_NAME; $page->process = $this; $page->save(); // tell the user we created this page $this->message("Created Page: {$page->path}"); } // we create the permission blog to limit access to the module $permission = $this->permissions->get('blog'); if (!$permission->id) { $p = new Permission(); $p->name = 'blog'; $p->title = $this->_('View Blog Page'); $p->save(); // tell the user we created this module's permission $this->message("Created New Permission: blog"); } // save initial module configurations $this->wire('modules')->saveModuleConfigData($this, self::configDefaults()); } In other modules I even call external scripts from within install().
    1 point
  11. You can create a own logout template/page with this content: <?php $session->logout(); $session->redirect('/');
    1 point
  12. I usually use regular pages for categories, and keep categories in a completely separate sub-section of the page tree. This requires two templates: category-index: This is the "master" page, which holds all the categories. Set to allow only one page, and only allow children of type "category". category: Each category page represents a category. Category pages are set to allow only the category-index as it's parent page – or allow other category pages, this way you can build hierarchical categories. Then you only need a Page Reference field targeting the category pages. This field can either be a single-option select if you only want to allow one category per post, or a multivalue field for multiple categories. The benefit of this approach is that editors can easily create new categories, and you can use normal page access controls to decide who can edit and add categories. You can easily change their sort setting to sort categories alphabetically or whatever you want. Pages are also way easier to access, search for and translate from the API side. For example, getting a list of categories is as easy as: $pages->get('template=category-index')->children()
    1 point
  13. Handle yourself (in code) or use the whitelist option, in which case ProcessWire will handle them automagically for you — but anyway, whitelist is also explained on that page ?
    1 point
  14. Thanks for reporting and testing - v0.3.2 has a fix for when CKEditor fields have settings overrides per Repeater Matrix type.
    1 point
  15. Good day everybody! Thanks for the module @ryan! Is there an easy way to integrate social login into this?
    1 point
  16. It's probably doable, but I see 2 potential problems for it. The first one is missing structure. ProcessWire is super flexible, which means you won't have any guidelines how to structure whatever you're building in a way that your customers will be happy with it. For customers this also means more overhead in trying to apply a certain bought solution, as each might work differently. The other part is market size. Themeforest and people on the platform do make their money by volume. Wordpress and HTML Templates are great for volume sales. ProcessWire doesn't have that kind of adoption numbers. This might be a classical chicken/egg problem in that not having things on themeforest might keep adoption low, but if you're looking to make money (in a reasonable timeframe) I'm not sure concentrating on ProcessWire in a setting like Themeforest is the best bet.
    1 point
  17. Steps to solve this: create .js file which sends your sort criteria via form and receives the updated page list. Reload page or update content (ajax) get the input (sort criteria) via PW API: $input->post sanitize your input select pages from DB $pages->find('template=xy, parent=xy,sort=criteria1,sort=criteria2, ... and so on); render updated list.
    1 point
  18. Wohoooo! So cool guys! Now it's #2!!!!!! Right after Joomla! ????❤️??
    1 point
  19. @Robin S I agree and this is already in progress. It's not in the first version but is in the next one—here's the relevant section from the draft documentation:
    1 point
  20. Yeah, @Robin S is mostly correct. The array approach seems like a good idea, but it will end up biting you in many cases. For ease of readability and building up complex selectors, I do build them with a regular php array and then simply implode the array with a comma to get the string selector. This is now my goto approach for any complex dynamic selectors.
    1 point
  21. Just a follow up in case anyone is interested. On a separate project I needed to get into grouped OR selectors and I decided the easiest way is to create a standard array and simply do an implode on it, eg: $selector = []; $selector[] = 'user_types='.$u->user_type; $selector[] = 'ages=(ages.count='.$pages->count('template=age').'), ages=(ages='.$u->age).')'; $selector[] = 'sexes=(sexes.count='.$pages->count('template=sex').'), sexes=(sexes='.$u->sex.')'; $selector = implode(', ', $selector); This is back to how I used to do things and I feel like this is almost as clean as the selector array approach and so much more flexible.
    1 point
  22. Hi All, Recently I've been trying to improve my development skills with R&D projects. My skills are mostly with HTML, PHP and Javascript. I class myself as a front-end developer however I can build back-end applications. I remember when Web 2.0 was realised and there was a shift in ideology and a design guideline to come along with it. But recently things are moving quicker than ever. I'm seeing a huge amount of people dropping jQuery in favour of ES6. But more than that, they are using framework's like React, Angular and Vue. Recently I learnt about JAMStack, and idea that you use API's to generate a flat file website which you serve through an CDN and honestly, I'm feeling way out of my depth here. I really want to stay up to date, but it seems developing a website is like engineering an engine, once you get to the top end of "speed" making it go faster is 100x harder. Don't get me wrong, I'm happy with the performance of ProcessWire, but these JAMStack websites are blazing. I guess you could call this Web 3.0 and it's all about speed and lightweight. Things like ASYNC and lot of other things that can come together to make a pleasurable experience even on the slowest of mobile networks. Is all of this a fad or is it really the next big movement? Is that how we should be developing websites now, or are they just buzz-words? I do like the idea of the CMS generating flat files and routing that through a CDN, but also using Git to manage all the changes it seems very fluid. (https://www.netlify.com, https://github.com/netlify/netlify-cms). I would love to learn all of this, but I feel like there is so much to catch up on I don't know where to start. Like, what is gulp? postCSS? I guess they are pre-processing like LESS to have all this power but still serve small files over the browser. As you can probably tell, I'm overwhelmed by all these things and it doesn't stop, now the "next big thing" is Gatsby (https://www.gatsbyjs.org) and whatever happened to Node.js? I feel like I'm going crazy haha! My question is, how do you guys deal with all of this? Is it something that ProcessWire can be used with as ProcessWire uses a database and requires PHP. I recently read: which touched upon these things, but I didn't understand it all.
    1 point
  23. Hey @Tom. JAMstack itself is just a marketing term created by Netlify’s CEO to onboard people onto their hosting platform and CMS. Netlify CMS is not really a CMS, it is an admin panel that integrates with static site generators like Hugo and Jekyll. The use case for static site generators is usually very simple sites with basic structured content like blogs and portfolios. See examples here https://jamstack.org/examples/ The JAMstack methodology also encourages using cloud CMS platforms like Contentful, however there are benefits and drawbacks to this I won't go into. To set up something like what you are asking about (but way more fun and flexible) with Processwire, imagine you have a domain http://api.website.com pointed to a Processwire instance with a homepage template like this: <?php namespace ProcessWire; header('HTTP/1.1 200'); header('Content-Type: application/json,charset=utf-8'); header("access-control-allow-origin: *"); $data = []; $projects = $pages->find("template=project"); foreach ($projects as $p) { $data[] = [ $title -> $p->title; $content -> $p->sometextareafield; ] } echo json_encode($data); and the main domain http://website.com is just a static hosted at netlify and your index.html contains the below: <body> <div id="projects"></div> <script type="text/javascript"> $(document).ready(function() { $.ajax({url: "http://api.website.com"}).done(function(data) { var $projects = $('#projects'); data.forEach(function(project) { var html = "<div class='project'>" + "<h2>" + project.title + "</h2>" + project.content + "</div>"; $projects.append(html); }); }); }); </script> </body> ... then you have the beginnings of a single page javascript app. Of course building an entire site like this and incorporating routing and state management etc would become tiresome very quickly in jQuery, which is why javascript frameworks like React and Vue exist. Regarding build tools and task runners like Webpack and Gulp, they exist to do what they say, i.e. bundle up static assets or run tasks. Before getting to that tho, it is important to understand the way people use node and npm for front end dev. See this article maybe: https://www.impressivewebs.com/npm-for-beginners-a-guide-for-front-end-developers/ I personally hate Webpack, I use npm scripts and the CLIs of my preferred libraries to do everything. I can post an example if you like, but these blog posts sum it up: https://deliciousbrains.com/npm-build-script/ https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/
    1 point
  24. I went "crazy" quite a few times over all frontend churn as well, FOMO, etc. At the end of the day, use the best tool for the job. Right now, practically all of my needs are met with the following stack: ProcessWire UIkit3 (it's so good; but, but the horror of using UIkits classes in your HTML!) Some PW premium modules: ListerPro ProCache (it's become my build process tool; you can build UIkit3 with it perfectly fine; I'm dumping Node, Webpack, and whatever the "latest" way to do things is) FormBuilder Pulling in whatever packages I need via composer VueJS if needed (loading it via CDN, oh my god the horror!) Digital Ocean for hosting (manually configuring their Ubuntu LAMP 16.04 droplet... takes only a few minutes) If your clients want to use some crappy shared host, switch them to DigitalOcean; it's practically the same price. I develop all my sites on Windows (but technically it's Linux thanks to WSL). No VMs, no Docker (again, the horror of what happens if my server ends up being PHP v7.0.23 and my local machine is 7.0.22!). Simple and reliable 99.99999999999999% of the time. I deploy with a simple shell script (horror! not using Capistrano, Ansible, whatever the latest tool is). Sure, your site won't be able to handle all of earth's traffic if everyone were to visit your site at the same moment, but it'll be fine for 85.7% of the sites you most likely take on. I've built very complex sites with just those tools and I move extremely rapidly while keeping the site client-maintainable! Even if you became Mr. Expert on the latest frontend tools, it's going to change significantly by this time next year and the year after that. Perhaps sit on the sidelines for a bit. End rant.
    1 point
  25. Well, I'm no pro at this and you could probably improve it, but here's my attempt which serves my current needs pretty well: /** * Creates a repeater field with associated fieldgroup, template, and page * * @param string $repeaterName The name of your repeater field * @param string $repeaterFields List of field names to add to the repeater, separated by spaces * @param string $repeaterLabel The label for your repeater * @param string $repeaterTags Tags for the repeater field * @return Returns the new Repeater field * */ public function createRepeater($repeaterName,$repeaterFields,$repeaterLabel,$repeaterTags) { $fieldsArray = explode(' ',$repeaterFields); $f = new Field(); $f->type = $this->modules->get("FieldtypeRepeater"); $f->name = $repeaterName; $f->label = $repeaterLabel; $f->tags = $repeaterTags; $f->repeaterReadyItems = 3; //Create fieldgroup $repeaterFg = new Fieldgroup(); $repeaterFg->name = "repeater_$repeaterName"; //Add fields to fieldgroup foreach($fieldsArray as $field) { $repeaterFg->append($this->fields->get($field)); } $repeaterFg->save(); //Create template $repeaterT = new Template(); $repeaterT->name = "repeater_$repeaterName"; $repeaterT->flags = 8; $repeaterT->noChildren = 1; $repeaterT->noParents = 1; $repeaterT->noGlobal = 1; $repeaterT->slashUrls = 1; $repeaterT->fieldgroup = $repeaterFg; $repeaterT->save(); //Setup page for the repeater - Very important $repeaterPage = "for-field-{$f->id}"; $f->parent_id = $this->pages->get("name=$repeaterPage")->id; $f->template_id = $repeaterT->id; $f->repeaterReadyItems = 3; //Now, add the fields directly to the repeater field foreach($fieldsArray as $field) { $f->repeaterFields = $this->fields->get($field); } $f->save(); return $f; } And here's an example of calling it: $f = $this->createRepeater("sc_promos","sc_promo_active sc_promo_code sc_promo_discount","Promotional Offer","shoppingCart"); You can then use $f to add your new repeater field to a fieldgroup/template.
    1 point
  26. Good find - I had forgotten about that post I just used the code from that other post of mine on a fresh PW install (latest dev, although I think it should work on 2.3 stable as well) and it works perfectly. I know that when I was playing around with writing that code I often messed up the PW database to the point where I was getting errors and found the best way was to do a fresh install again. BUT, I just noticed that you are missing these key lines in your version: $repeater_page = "for-field-{$f->id}"; $f->parent_id = $this->pages->get("name=$repeater_page")->id; $f->template_id = $repeater_template->id; $f->repeaterReadyItems = 3; This is critical to make things work. Let me know how you go.
    1 point
×
×
  • Create New...